Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Lovato <nathan@gdquest.com>2019-09-05 18:22:37 +0300
committerNathan Lovato <nathan@gdquest.com>2019-09-05 18:22:52 +0300
commit61d48c0a4be0ab8f71e6e1d35f0aa99c77fcfd33 (patch)
treef64d0ea88789184f45112b489598990d549788f2 /power_sequencer
parentda5a1175e30c347fbce05e49e2f5f895be30bd5b (diff)
Add the VSE addon Power Sequencer
Diffstat (limited to 'power_sequencer')
-rw-r--r--power_sequencer/__init__.py84
-rw-r--r--power_sequencer/addon_preferences.py48
-rw-r--r--power_sequencer/addon_properties.py46
-rw-r--r--power_sequencer/handlers.py89
-rw-r--r--power_sequencer/operators/__init__.py165
-rw-r--r--power_sequencer/operators/align_audios.py109
-rw-r--r--power_sequencer/operators/audiosync/__init__.py17
-rw-r--r--power_sequencer/operators/audiosync/convert_and_trim.py61
-rw-r--r--power_sequencer/operators/audiosync/cross_correlation.py33
-rw-r--r--power_sequencer/operators/audiosync/ensure_non_zero.py28
-rw-r--r--power_sequencer/operators/audiosync/find_offset.py87
-rw-r--r--power_sequencer/operators/audiosync/mfcc/__init__.py17
-rw-r--r--power_sequencer/operators/audiosync/mfcc/mfcc.py92
-rw-r--r--power_sequencer/operators/audiosync/mfcc/segment_axis.py110
-rw-r--r--power_sequencer/operators/audiosync/mfcc/trfbank.py51
-rw-r--r--power_sequencer/operators/audiosync/std_mfcc.py21
-rw-r--r--power_sequencer/operators/channel_offset.py79
-rw-r--r--power_sequencer/operators/concatenate_strips.py154
-rw-r--r--power_sequencer/operators/copy_selected_sequences.py95
-rw-r--r--power_sequencer/operators/crossfade_add.py119
-rw-r--r--power_sequencer/operators/crossfade_edit.py89
-rw-r--r--power_sequencer/operators/cut_strips_under_cursor.py65
-rw-r--r--power_sequencer/operators/delete_direct.py74
-rw-r--r--power_sequencer/operators/deselect_all_left_or_right.py86
-rw-r--r--power_sequencer/operators/deselect_handles_and_grab.py50
-rw-r--r--power_sequencer/operators/duplicate_move.py56
-rw-r--r--power_sequencer/operators/expand_to_surrounding_cuts.py104
-rw-r--r--power_sequencer/operators/fade_add.py254
-rw-r--r--power_sequencer/operators/fade_clear.py64
-rw-r--r--power_sequencer/operators/gap_remove.py135
-rw-r--r--power_sequencer/operators/grab.py66
-rw-r--r--power_sequencer/operators/grab_closest_handle_or_cut.py110
-rw-r--r--power_sequencer/operators/grab_sequence_handles.py88
-rw-r--r--power_sequencer/operators/import_local_footage.py268
-rw-r--r--power_sequencer/operators/jump_time_offset.py82
-rw-r--r--power_sequencer/operators/jump_to_cut.py119
-rw-r--r--power_sequencer/operators/make_still_image.py121
-rw-r--r--power_sequencer/operators/marker_delete_closest.py49
-rw-r--r--power_sequencer/operators/marker_delete_direct.py50
-rw-r--r--power_sequencer/operators/marker_go_to_next.py74
-rw-r--r--power_sequencer/operators/marker_snap_to_cursor.py61
-rw-r--r--power_sequencer/operators/markers_as_timecodes.py67
-rw-r--r--power_sequencer/operators/markers_create_from_selected.py59
-rw-r--r--power_sequencer/operators/markers_set_preview_in_between.py69
-rw-r--r--power_sequencer/operators/markers_snap_matching_strips.py50
-rw-r--r--power_sequencer/operators/meta_resize_to_content.py51
-rw-r--r--power_sequencer/operators/meta_trim_content_to_bounds.py63
-rw-r--r--power_sequencer/operators/meta_ungroup_and_trim.py60
-rw-r--r--power_sequencer/operators/mouse_toggle_mute.py64
-rw-r--r--power_sequencer/operators/mouse_trim_instantly.py113
-rw-r--r--power_sequencer/operators/mouse_trim_modal.py408
-rw-r--r--power_sequencer/operators/open_project_directory.py59
-rw-r--r--power_sequencer/operators/playback_speed_decrease.py63
-rw-r--r--power_sequencer/operators/playback_speed_increase.py63
-rw-r--r--power_sequencer/operators/playback_speed_set.py68
-rw-r--r--power_sequencer/operators/preview_closest_cut.py94
-rw-r--r--power_sequencer/operators/preview_to_selection.py56
-rw-r--r--power_sequencer/operators/render_apply_preset.py128
-rw-r--r--power_sequencer/operators/render_presets/twitter_720p.py47
-rw-r--r--power_sequencer/operators/render_presets/youtube_1080.py47
-rw-r--r--power_sequencer/operators/ripple_delete.py111
-rw-r--r--power_sequencer/operators/save_direct.py49
-rw-r--r--power_sequencer/operators/scene_create_from_selection.py87
-rw-r--r--power_sequencer/operators/scene_cycle.py54
-rw-r--r--power_sequencer/operators/scene_merge_from.py108
-rw-r--r--power_sequencer/operators/scene_open_from_strip.py53
-rw-r--r--power_sequencer/operators/scene_rename_with_strip.py60
-rw-r--r--power_sequencer/operators/select_all_left_or_right.py65
-rw-r--r--power_sequencer/operators/select_closest_to_mouse.py58
-rw-r--r--power_sequencer/operators/select_linked_effect.py47
-rw-r--r--power_sequencer/operators/select_linked_strips.py70
-rw-r--r--power_sequencer/operators/select_related_strips.py140
-rw-r--r--power_sequencer/operators/select_strips_under_cursor.py51
-rw-r--r--power_sequencer/operators/set_timeline_range.py56
-rw-r--r--power_sequencer/operators/snap.py58
-rw-r--r--power_sequencer/operators/snap_selection.py58
-rw-r--r--power_sequencer/operators/space_sequences.py66
-rw-r--r--power_sequencer/operators/speed_remove_effect.py64
-rw-r--r--power_sequencer/operators/speed_up_movie_strip.py127
-rw-r--r--power_sequencer/operators/swap_strips.py247
-rw-r--r--power_sequencer/operators/synchronize_titles.py112
-rw-r--r--power_sequencer/operators/toggle_selected_mute.py71
-rw-r--r--power_sequencer/operators/toggle_waveforms.py82
-rw-r--r--power_sequencer/operators/transitions_remove.py85
-rw-r--r--power_sequencer/operators/trim_left_or_right_handles.py115
-rw-r--r--power_sequencer/operators/trim_three_point_edit.py66
-rw-r--r--power_sequencer/operators/trim_to_surrounding_cuts.py164
-rw-r--r--power_sequencer/operators/utils/__init__.py16
-rw-r--r--power_sequencer/operators/utils/doc.py58
-rw-r--r--power_sequencer/operators/utils/draw.py121
-rw-r--r--power_sequencer/operators/utils/functions.py343
-rw-r--r--power_sequencer/operators/utils/global_settings.py110
-rw-r--r--power_sequencer/operators/utils/info_progress_bar.py72
-rw-r--r--power_sequencer/scripts/BPSProxy/bpsproxy/__init__.py16
-rw-r--r--power_sequencer/scripts/BPSProxy/bpsproxy/__main__.py171
-rw-r--r--power_sequencer/scripts/BPSProxy/bpsproxy/call.py95
-rw-r--r--power_sequencer/scripts/BPSProxy/bpsproxy/commands.py190
-rw-r--r--power_sequencer/scripts/BPSProxy/bpsproxy/config.py41
-rw-r--r--power_sequencer/scripts/BPSProxy/bpsproxy/utils.py109
-rw-r--r--power_sequencer/scripts/BPSProxy/setup.py55
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/__init__.py17
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/__main__.py147
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/bscripts/mixdown.py30
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/bscripts/probe.py30
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/bscripts/video.py19
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/calls.py410
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/commands.py341
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/config.py37
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/helpers.py110
-rw-r--r--power_sequencer/scripts/BPSRender/bpsrender/setup.py182
-rw-r--r--power_sequencer/scripts/BPSRender/setup.py55
-rw-r--r--power_sequencer/ui/__init__.py48
-rw-r--r--power_sequencer/ui/menu_contextual.py83
-rw-r--r--power_sequencer/ui/menu_toolbar.py204
-rw-r--r--power_sequencer/utils/addon_auto_imports.py72
-rw-r--r--power_sequencer/utils/register_shortcuts.py62
116 files changed, 10767 insertions, 0 deletions
diff --git a/power_sequencer/__init__.py b/power_sequencer/__init__.py
new file mode 100644
index 00000000..7bc1c6ca
--- /dev/null
+++ b/power_sequencer/__init__.py
@@ -0,0 +1,84 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .addon_preferences import register_preferences, unregister_preferences
+from .addon_properties import register_properties, unregister_properties
+from .operators import classes
+from .utils.register_shortcuts import register_shortcuts
+from .handlers import register_handlers, unregister_handlers
+from .utils import addon_auto_imports
+from .ui import register_ui, unregister_ui
+
+
+# load and reload submodules
+##################################
+modules = addon_auto_imports.setup_addon_modules(
+ __path__, __name__, ignore_packages=[".utils", ".audiosync"], ignore_modules=[]
+)
+
+
+bl_info = {
+ "name": "Power Sequencer",
+ "description": "Video editing tools for content creators",
+ "author": "Nathan Lovato",
+ "version": (1, 4, 0),
+ "blender": (2, 80, 0),
+ "location": "Sequencer",
+ "tracker_url": "https://github.com/GDquest/Blender-power-sequencer/issues",
+ "wiki_url": "https://www.youtube.com/playlist?list=PLhqJJNjsQ7KFjp88Cu57Zb9_wFt7nlkEI",
+ "support": "COMMUNITY",
+ "category": "Sequencer",
+}
+
+
+addon_keymaps = []
+
+
+def register():
+ global addon_keymaps
+
+ register_preferences()
+ register_properties()
+ register_handlers()
+ register_ui()
+
+ for c in classes:
+ bpy.utils.register_class(c)
+
+ keymaps = register_shortcuts()
+ addon_keymaps += keymaps
+
+ print("Registered {} with {} modules".format(bl_info["name"], len(modules)))
+
+
+def unregister():
+ global addon_keymaps
+
+ for km, kmi in addon_keymaps:
+ km.keymap_items.remove(kmi)
+ addon_keymaps.clear()
+
+ for c in classes:
+ bpy.utils.unregister_class(c)
+
+ unregister_ui()
+ unregister_preferences()
+ unregister_properties()
+ unregister_handlers()
+
+ print("Unregistered {}".format(bl_info["name"]))
diff --git a/power_sequencer/addon_preferences.py b/power_sequencer/addon_preferences.py
new file mode 100644
index 00000000..0e8e7d71
--- /dev/null
+++ b/power_sequencer/addon_preferences.py
@@ -0,0 +1,48 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Add-on preferences and interface in the Blender preferences window.
+"""
+import bpy
+
+
+def get_preferences(context):
+ return context.preferences.addons[__package__].preferences
+
+class PowerSequencerPreferences(bpy.types.AddonPreferences):
+ bl_idname = __package__
+
+ proxy_25: bpy.props.BoolProperty(name="25%", default=False)
+ proxy_50: bpy.props.BoolProperty(name="50%", default=False)
+ proxy_75: bpy.props.BoolProperty(name="75%", default=False)
+ proxy_100: bpy.props.BoolProperty(name="100%", default=False)
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.label(text="Proxy")
+
+ row = layout.row()
+ row.prop(self, "proxy_25")
+ row.prop(self, "proxy_50")
+ row.prop(self, "proxy_75")
+ row.prop(self, "proxy_100")
+
+
+register_preferences, unregister_preferences = bpy.utils.register_classes_factory(
+ [PowerSequencerPreferences]
+)
diff --git a/power_sequencer/addon_properties.py b/power_sequencer/addon_properties.py
new file mode 100644
index 00000000..06cbe1da
--- /dev/null
+++ b/power_sequencer/addon_properties.py
@@ -0,0 +1,46 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+
+class PowerSequencerProperties(bpy.types.PropertyGroup):
+ playback_speed: bpy.props.EnumProperty(
+ items=[
+ ("NORMAL", "Normal (1x)", ""),
+ ("FAST", "Fast (1.33x)", ""),
+ ("FASTER", "Faster (1.66x)", ""),
+ ("DOUBLE", "Double (2x)", ""),
+ ("TRIPLE", "Triple (3x)", ""),
+ ],
+ name="Playback",
+ default="NORMAL",
+ )
+
+ frame_pre: bpy.props.IntProperty(name="Frame before frame_change", default=0, min=0)
+
+ active_tab: bpy.props.StringProperty(
+ name="Active Tab", description="The name of the active tab in the UI", default="Sequencer"
+ )
+
+
+def register_properties():
+ bpy.utils.register_class(PowerSequencerProperties)
+ bpy.types.Scene.power_sequencer = bpy.props.PointerProperty(type=PowerSequencerProperties)
+
+
+def unregister_properties():
+ bpy.utils.unregister_class(PowerSequencerProperties)
diff --git a/power_sequencer/handlers.py b/power_sequencer/handlers.py
new file mode 100644
index 00000000..8e93bb27
--- /dev/null
+++ b/power_sequencer/handlers.py
@@ -0,0 +1,89 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from bpy.app.handlers import persistent
+
+
+@persistent
+def power_sequencer_load_file_post(arg):
+ """
+ Called after loading the blend file
+ """
+ for scene in bpy.data.scenes:
+ scene.power_sequencer.frame_pre = bpy.context.scene.frame_current
+
+
+@persistent
+def power_sequencer_playback_speed_post(scene):
+ """
+ Handler function for faster playback
+ Skips keyframes after a frame change based on the playback_speed value
+ It steps over frame rather than increase the playback speed smoothly,
+ but it's still useful for faster editing
+ """
+ if bpy.context.screen and not bpy.context.screen.is_animation_playing:
+ return
+
+ playback_speed = scene.power_sequencer.playback_speed
+
+ frame_start = scene.frame_current
+ frame_post = scene.frame_current
+
+ if playback_speed == "FAST" and frame_start % 3 == 0:
+ frame_post += 1
+ elif playback_speed == "FASTER" and frame_start % 2 == 0:
+ frame_post += 1
+ elif playback_speed == "DOUBLE":
+ # 2.5x -> skip 5 frames for 2. 2 then 3 then 2 etc.
+ frame_post += 1
+ elif playback_speed == "TRIPLE":
+ frame_post += 2
+
+ if frame_start != frame_post:
+ bpy.ops.screen.frame_offset(delta=frame_post - frame_start)
+ scene.power_sequencer.frame_pre = scene.frame_current
+
+
+def draw_playback_speed(self, context):
+ layout = self.layout
+ scene = context.scene
+ layout.prop(scene.power_sequencer, "playback_speed")
+
+
+def draw_ui_menu(self, context):
+ layout = self.layout
+ layout.menu("POWER_SEQUENCER_MT_main")
+
+
+def register_handlers():
+ # Menus
+ bpy.types.SEQUENCER_HT_header.append(draw_ui_menu)
+ bpy.types.SEQUENCER_HT_header.append(draw_playback_speed)
+
+ # Handlers
+ bpy.app.handlers.load_post.append(power_sequencer_load_file_post)
+ bpy.app.handlers.frame_change_post.append(power_sequencer_playback_speed_post)
+
+
+def unregister_handlers():
+ # Menus
+ bpy.types.SEQUENCER_HT_header.remove(draw_ui_menu)
+ bpy.types.SEQUENCER_HT_header.remove(draw_playback_speed)
+
+ # Handlers
+ bpy.app.handlers.load_post.remove(power_sequencer_load_file_post)
+ bpy.app.handlers.frame_change_post.remove(power_sequencer_playback_speed_post)
diff --git a/power_sequencer/operators/__init__.py b/power_sequencer/operators/__init__.py
new file mode 100644
index 00000000..7c1e0ca0
--- /dev/null
+++ b/power_sequencer/operators/__init__.py
@@ -0,0 +1,165 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+from .speed_up_movie_strip import POWER_SEQUENCER_OT_speed_up_movie_strip
+from .align_audios import POWER_SEQUENCER_OT_align_audios
+from .playback_speed_set import POWER_SEQUENCER_OT_playback_speed_set
+from .channel_offset import POWER_SEQUENCER_OT_channel_offset
+from .concatenate_strips import POWER_SEQUENCER_OT_concatenate_strips
+from .copy_selected_sequences import POWER_SEQUENCER_OT_copy_selected_sequences
+from .crossfade_add import POWER_SEQUENCER_OT_crossfade_add
+from .crossfade_edit import POWER_SEQUENCER_OT_crossfade_edit
+from .transitions_remove import POWER_SEQUENCER_OT_transitions_remove
+from .cut_strips_under_cursor import POWER_SEQUENCER_OT_split_strips_under_cursor
+from .playback_speed_decrease import POWER_SEQUENCER_OT_playback_speed_decrease
+from .delete_direct import POWER_SEQUENCER_OT_delete_direct
+from .deselect_all_left_or_right import POWER_SEQUENCER_OT_deselect_all_strips_left_or_right
+from .deselect_handles_and_grab import POWER_SEQUENCER_OT_deselect_handles_and_grab
+from .duplicate_move import POWER_SEQUENCER_OT_duplicate_move
+from .expand_to_surrounding_cuts import POWER_SEQUENCER_OT_expand_to_surrounding_cuts
+from .fade_add import POWER_SEQUENCER_OT_fade_add
+from .fade_clear import POWER_SEQUENCER_OT_fade_clear
+from .grab_closest_handle_or_cut import POWER_SEQUENCER_OT_grab_closest_cut
+from .grab import POWER_SEQUENCER_OT_grab
+from .grab_sequence_handles import POWER_SEQUENCER_OT_grab_sequence_handles
+from .import_local_footage import POWER_SEQUENCER_OT_import_local_footage
+from .playback_speed_increase import POWER_SEQUENCER_OT_playback_speed_increase
+from .jump_time_offset import POWER_SEQUENCER_OT_jump_time_offset
+from .jump_to_cut import POWER_SEQUENCER_OT_jump_to_cut
+from .make_still_image import POWER_SEQUENCER_OT_make_still_image
+from .marker_delete_closest import POWER_SEQUENCER_OT_marker_delete_closest
+from .marker_delete_direct import POWER_SEQUENCER_OT_marker_delete_direct
+from .marker_go_to_next import POWER_SEQUENCER_OT_marker_go_to_next
+from .markers_as_timecodes import POWER_SEQUENCER_OT_copy_markers_as_timecodes
+from .markers_create_from_selected import POWER_SEQUENCER_OT_markers_create_from_selected_strips
+from .marker_snap_to_cursor import POWER_SEQUENCER_OT_marker_snap_to_cursor
+from .markers_snap_matching_strips import POWER_SEQUENCER_OT_markers_snap_matching_strips
+from .meta_resize_to_content import POWER_SEQUENCER_OT_meta_resize_to_content
+from .meta_ungroup_and_trim import POWER_SEQUENCER_OT_meta_ungroup_and_trim
+from .meta_trim_content_to_bounds import POWER_SEQUENCER_OT_meta_trim_content_to_bounds
+from .mouse_trim_modal import POWER_SEQUENCER_OT_mouse_trim
+from .space_sequences import POWER_SEQUENCER_OT_space_sequences
+from .mouse_toggle_mute import POWER_SEQUENCER_OT_mouse_toggle_mute
+from .mouse_trim_instantly import POWER_SEQUENCER_OT_mouse_trim_instantly
+from .open_project_directory import POWER_SEQUENCER_OT_open_project_directory
+from .preview_closest_cut import POWER_SEQUENCER_OT_preview_closest_cut
+from .preview_to_selection import POWER_SEQUENCER_OT_preview_to_selection
+from .gap_remove import POWER_SEQUENCER_OT_gap_remove
+from .scene_rename_with_strip import POWER_SEQUENCER_OT_scene_rename_with_strip
+from .render_apply_preset import POWER_SEQUENCER_OT_render_apply_preset
+from .ripple_delete import POWER_SEQUENCER_OT_ripple_delete
+from .save_direct import POWER_SEQUENCER_OT_save_direct
+from .scene_create_from_selection import POWER_SEQUENCER_OT_scene_create_from_selection
+from .scene_cycle import POWER_SEQUENCER_OT_scene_cycle
+from .select_closest_to_mouse import POWER_SEQUENCER_OT_select_closest_to_mouse
+from .select_linked_strips import POWER_SEQUENCER_OT_select_linked_strips
+from .select_linked_effect import POWER_SEQUENCER_OT_select_linked_effect
+from .select_related_strips import POWER_SEQUENCER_OT_select_related_strips
+from .select_strips_under_cursor import POWER_SEQUENCER_OT_select_strips_under_cursor
+from .markers_set_preview_in_between import POWER_SEQUENCER_OT_set_preview_between_markers
+from .set_timeline_range import POWER_SEQUENCER_OT_set_timeline_range
+from .trim_left_or_right_handles import POWER_SEQUENCER_OT_trim_left_or_right_handles
+from .snap import POWER_SEQUENCER_OT_snap
+from .snap_selection import POWER_SEQUENCER_OT_snap_selection
+from .speed_remove_effect import POWER_SEQUENCER_OT_speed_remove_effect
+from .swap_strips import POWER_SEQUENCER_OT_swap_strips
+from .select_all_left_or_right import POWER_SEQUENCER_OT_select_all_left_or_right
+from .synchronize_titles import POWER_SEQUENCER_OT_synchronize_titles
+from .toggle_selected_mute import POWER_SEQUENCER_OT_toggle_selected_mute
+from .toggle_waveforms import POWER_SEQUENCER_OT_toggle_waveforms
+from .trim_three_point_edit import POWER_SEQUENCER_OT_trim_three_point_edit
+from .trim_to_surrounding_cuts import POWER_SEQUENCER_OT_trim_to_surrounding_cuts
+
+classes = [
+ POWER_SEQUENCER_OT_speed_up_movie_strip,
+ POWER_SEQUENCER_OT_align_audios,
+ POWER_SEQUENCER_OT_playback_speed_set,
+ POWER_SEQUENCER_OT_channel_offset,
+ POWER_SEQUENCER_OT_concatenate_strips,
+ POWER_SEQUENCER_OT_copy_selected_sequences,
+ POWER_SEQUENCER_OT_crossfade_add,
+ POWER_SEQUENCER_OT_crossfade_edit,
+ POWER_SEQUENCER_OT_transitions_remove,
+ POWER_SEQUENCER_OT_split_strips_under_cursor,
+ POWER_SEQUENCER_OT_playback_speed_decrease,
+ POWER_SEQUENCER_OT_delete_direct,
+ POWER_SEQUENCER_OT_deselect_all_strips_left_or_right,
+ POWER_SEQUENCER_OT_deselect_handles_and_grab,
+ POWER_SEQUENCER_OT_duplicate_move,
+ POWER_SEQUENCER_OT_expand_to_surrounding_cuts,
+ POWER_SEQUENCER_OT_fade_add,
+ POWER_SEQUENCER_OT_fade_clear,
+ POWER_SEQUENCER_OT_grab_closest_cut,
+ POWER_SEQUENCER_OT_grab,
+ POWER_SEQUENCER_OT_grab_sequence_handles,
+ POWER_SEQUENCER_OT_import_local_footage,
+ POWER_SEQUENCER_OT_playback_speed_increase,
+ POWER_SEQUENCER_OT_jump_time_offset,
+ POWER_SEQUENCER_OT_jump_to_cut,
+ POWER_SEQUENCER_OT_make_still_image,
+ POWER_SEQUENCER_OT_marker_delete_closest,
+ POWER_SEQUENCER_OT_marker_delete_direct,
+ POWER_SEQUENCER_OT_marker_go_to_next,
+ POWER_SEQUENCER_OT_copy_markers_as_timecodes,
+ POWER_SEQUENCER_OT_markers_create_from_selected_strips,
+ POWER_SEQUENCER_OT_marker_snap_to_cursor,
+ POWER_SEQUENCER_OT_markers_snap_matching_strips,
+ POWER_SEQUENCER_OT_meta_resize_to_content,
+ POWER_SEQUENCER_OT_meta_ungroup_and_trim,
+ POWER_SEQUENCER_OT_meta_trim_content_to_bounds,
+ POWER_SEQUENCER_OT_mouse_trim,
+ POWER_SEQUENCER_OT_space_sequences,
+ POWER_SEQUENCER_OT_mouse_toggle_mute,
+ POWER_SEQUENCER_OT_mouse_trim_instantly,
+ POWER_SEQUENCER_OT_open_project_directory,
+ POWER_SEQUENCER_OT_preview_closest_cut,
+ POWER_SEQUENCER_OT_preview_to_selection,
+ POWER_SEQUENCER_OT_gap_remove,
+ POWER_SEQUENCER_OT_scene_rename_with_strip,
+ POWER_SEQUENCER_OT_render_apply_preset,
+ POWER_SEQUENCER_OT_ripple_delete,
+ POWER_SEQUENCER_OT_save_direct,
+ POWER_SEQUENCER_OT_scene_create_from_selection,
+ POWER_SEQUENCER_OT_scene_cycle,
+ POWER_SEQUENCER_OT_select_closest_to_mouse,
+ POWER_SEQUENCER_OT_select_linked_strips,
+ POWER_SEQUENCER_OT_select_linked_effect,
+ POWER_SEQUENCER_OT_select_related_strips,
+ POWER_SEQUENCER_OT_select_strips_under_cursor,
+ POWER_SEQUENCER_OT_set_preview_between_markers,
+ POWER_SEQUENCER_OT_set_timeline_range,
+ POWER_SEQUENCER_OT_trim_left_or_right_handles,
+ POWER_SEQUENCER_OT_snap,
+ POWER_SEQUENCER_OT_snap_selection,
+ POWER_SEQUENCER_OT_speed_remove_effect,
+ POWER_SEQUENCER_OT_swap_strips,
+ POWER_SEQUENCER_OT_synchronize_titles,
+ POWER_SEQUENCER_OT_toggle_selected_mute,
+ POWER_SEQUENCER_OT_toggle_waveforms,
+ POWER_SEQUENCER_OT_trim_three_point_edit,
+ POWER_SEQUENCER_OT_select_all_left_or_right,
+ POWER_SEQUENCER_OT_trim_to_surrounding_cuts,
+]
+
+doc = {
+ "sequencer.refresh_all": {
+ "name": "Refresh All",
+ "description": "",
+ "shortcuts": [({"type": "R", "value": "PRESS", "shift": True}, {}, "Refresh All")],
+ "demo": "",
+ "keymap": "Sequencer",
+ }
+}
diff --git a/power_sequencer/operators/align_audios.py b/power_sequencer/operators/align_audios.py
new file mode 100644
index 00000000..a280f1fd
--- /dev/null
+++ b/power_sequencer/operators/align_audios.py
@@ -0,0 +1,109 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_align_audios(bpy.types.Operator):
+ """*brief* Align two audio strips
+
+
+ Tries to synchronize the selected audio strip to the active audio strip by comparing the sound.
+ Useful to synchronize audio of the same event recorded with different microphones.
+
+ To use this feature, you must have [ffmpeg](https://www.ffmpeg.org/download.html) and
+ [scipy](https://www.scipy.org/install.html) installed on your computer and available on the PATH (command line) to work.
+
+ The longer the audio files, the longer the tool can take to run, as it has to convert, analyze,
+ and compare the audio sources to work.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/xkBUzDj.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ if not context.scene:
+ return False
+
+ active = context.scene.sequence_editor.active_strip
+ selected = context.selected_sequences
+ ok = (
+ len(selected) == 2
+ and active in selected
+ and all(map(lambda s: s.type == "SOUND", selected))
+ )
+ return ok
+
+ def execute(self, context):
+ try:
+ import scipy
+ except ImportError:
+ self.report({"ERROR"}, "Scipy must be installed to align audios")
+ return {"FINISHED"}
+
+ if not is_ffmpeg_available():
+ self.report({"ERROR"}, "ffmpeg must be installed to align audios")
+ return {"FINISHED"}
+
+ # This import is here because otherwise, it slows down blender startup
+ from .audiosync import find_offset
+
+ scene = context.scene
+
+ active = scene.sequence_editor.active_strip
+ active_filepath = bpy.path.abspath(active.sound.filepath)
+
+ selected = context.selected_sequences
+ selected.pop(selected.index(active))
+
+ align_strip = selected[0]
+ align_strip_filepath = bpy.path.abspath(align_strip.sound.filepath)
+
+ offset, score = find_offset(align_strip_filepath, active_filepath)
+
+ initial_offset = active.frame_start - align_strip.frame_start
+
+ fps = scene.render.fps / scene.render.fps_base
+ frames = int(offset * fps)
+
+ align_strip.frame_start -= frames - initial_offset
+
+ self.report({"INFO"}, "Alignment score: " + str(round(score, 1)))
+
+ return {"FINISHED"}
+
+
+def is_ffmpeg_available():
+ """
+ Returns true if ffmpeg is installed and available from the PATH
+ """
+ try:
+ subprocess.call(["ffmpeg", "--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ return True
+ except OSError:
+ return False
diff --git a/power_sequencer/operators/audiosync/__init__.py b/power_sequencer/operators/audiosync/__init__.py
new file mode 100644
index 00000000..9fc19da4
--- /dev/null
+++ b/power_sequencer/operators/audiosync/__init__.py
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+from .find_offset import find_offset
diff --git a/power_sequencer/operators/audiosync/convert_and_trim.py b/power_sequencer/operators/audiosync/convert_and_trim.py
new file mode 100644
index 00000000..1ce1a4c1
--- /dev/null
+++ b/power_sequencer/operators/audiosync/convert_and_trim.py
@@ -0,0 +1,61 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import os
+import subprocess
+import tempfile
+
+
+def convert_and_trim(audio_filepath, freq, dur):
+ """
+ Uses ffmpeg to convert an audio file to a temporary wav file for use
+ in finding offset.
+
+ Args
+ :audio: path to the audiofile to convert (string)
+ :freq: Samples / second in the output wav (int)
+ :dur: Max duration of the output wav in seconds (float)
+
+ Returns
+ :outpath: path to the output wav file
+ """
+
+ tmp = tempfile.NamedTemporaryFile(mode="r+b", prefix="offset_", suffix=".wav")
+ outpath = tmp.name
+ tmp.close()
+
+ channel_count = "1"
+
+ subprocess.call(
+ [
+ "ffmpeg",
+ "-loglevel",
+ "panic",
+ "-i",
+ audio_filepath,
+ "-ac",
+ channel_count,
+ "-ar",
+ str(freq),
+ "-t",
+ str(dur),
+ "-acodec",
+ "pcm_s16le",
+ outpath,
+ ]
+ )
+
+ return outpath
diff --git a/power_sequencer/operators/audiosync/cross_correlation.py b/power_sequencer/operators/audiosync/cross_correlation.py
new file mode 100644
index 00000000..b18ab35e
--- /dev/null
+++ b/power_sequencer/operators/audiosync/cross_correlation.py
@@ -0,0 +1,33 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import numpy as np
+
+
+def cross_correlation(mfcc1, mfcc2, nframes):
+ n1, mdim1 = mfcc1.shape
+ # n2, mdim2 = mfcc2.shape
+
+ n = n1 - nframes + 1
+
+ if n < 0:
+ return None
+
+ c = np.zeros(n)
+ for k in range(n):
+ cc = np.sum(np.multiply(mfcc1[k : k + nframes], mfcc2[:nframes]), axis=0)
+ c[k] = np.linalg.norm(cc)
+ return c
diff --git a/power_sequencer/operators/audiosync/ensure_non_zero.py b/power_sequencer/operators/audiosync/ensure_non_zero.py
new file mode 100644
index 00000000..911ab91b
--- /dev/null
+++ b/power_sequencer/operators/audiosync/ensure_non_zero.py
@@ -0,0 +1,28 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import numpy as np
+
+
+def ensure_non_zero(signal):
+ """
+ Adds a little bit of static to avoid
+ 'divide by zero encountered in log' during MFCC computation.
+ """
+
+ signal += np.random.random(len(signal)) * 10 ** -10
+
+ return signal
diff --git a/power_sequencer/operators/audiosync/find_offset.py b/power_sequencer/operators/audiosync/find_offset.py
new file mode 100644
index 00000000..a25bffde
--- /dev/null
+++ b/power_sequencer/operators/audiosync/find_offset.py
@@ -0,0 +1,87 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+This code is an adaptation of 'audio-offset-finder' by BBC
+"""
+import os
+import warnings
+import numpy as np
+from scipy.io import wavfile
+
+from .mfcc import mfcc
+from .convert_and_trim import convert_and_trim
+from .std_mfcc import std_mfcc
+from .cross_correlation import cross_correlation
+from .ensure_non_zero import ensure_non_zero
+
+
+def find_offset(file1, file2, freq=8000, trim=60 * 15, correl_nframes=1000):
+ """
+ Determine the offset (in seconds) between 2 audio files
+
+ Uses cross-correlation of standardised Mel-Frequency Cepstral
+ Coefficients
+ """
+ file1 = os.path.abspath(file1)
+ file2 = os.path.abspath(file2)
+
+ wav1_path = convert_and_trim(file1, freq, trim)
+ wav2_path = convert_and_trim(file2, freq, trim)
+
+ rate1, data1 = wavfile.read(wav1_path, mmap=True)
+ data1 = data1 / (2.0 ** 15)
+
+ rate2, data2 = wavfile.read(wav2_path, mmap=True)
+ data2 = data2 / (2.0 ** 15)
+
+ data1 = ensure_non_zero(data1)
+ data2 = ensure_non_zero(data2)
+
+ mfcc1 = mfcc(data1, nwin=256, nfft=512, fs=freq, nceps=13)[0]
+ mfcc2 = mfcc(data2, nwin=256, nfft=512, fs=freq, nceps=13)[0]
+
+ mfcc1 = std_mfcc(mfcc1)
+ mfcc2 = std_mfcc(mfcc2)
+
+ frames1 = mfcc1.shape[0]
+ frames2 = mfcc2.shape[0]
+
+ if frames1 > frames2:
+ flip = 1
+
+ else:
+ flip = -1
+ mfcc1, mfcc2 = mfcc2, mfcc1
+
+ c = cross_correlation(mfcc1, mfcc2, nframes=correl_nframes)
+ try:
+ c.any()
+ except AttributeError:
+ os.remove(wav1_path)
+ os.remove(wav2_path)
+
+ return 0, 0
+
+ max_k_index = np.argmax(c)
+
+ offset = max_k_index * 160.0 / float(freq)
+ score = (c[max_k_index] - np.mean(c)) / np.std(c)
+
+ os.remove(wav1_path)
+ os.remove(wav2_path)
+
+ return offset * flip, score
diff --git a/power_sequencer/operators/audiosync/mfcc/__init__.py b/power_sequencer/operators/audiosync/mfcc/__init__.py
new file mode 100644
index 00000000..2b20af43
--- /dev/null
+++ b/power_sequencer/operators/audiosync/mfcc/__init__.py
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+from .mfcc import mfcc
diff --git a/power_sequencer/operators/audiosync/mfcc/mfcc.py b/power_sequencer/operators/audiosync/mfcc/mfcc.py
new file mode 100644
index 00000000..93709e8c
--- /dev/null
+++ b/power_sequencer/operators/audiosync/mfcc/mfcc.py
@@ -0,0 +1,92 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import numpy as np
+
+from scipy.signal import hamming, lfilter
+from scipy.fftpack import fft
+from scipy.fftpack.realtransforms import dct
+
+from .trfbank import trfbank
+from .segment_axis import segment_axis
+
+
+def mfcc(input, nwin=256, nfft=512, fs=16000, nceps=13):
+ """Compute Mel Frequency Cepstral Coefficients.
+
+ Parameters
+ ----------
+ input: ndarray
+ input from which the coefficients are computed
+
+ Returns
+ -------
+ ceps: ndarray
+ Mel-cepstrum coefficients
+ mspec: ndarray
+ Log-spectrum in the mel-domain.
+
+ Notes
+ -----
+ MFCC are computed as follows:
+ * Pre-processing in time-domain (pre-emphasizing)
+ * Compute the spectrum amplitude by windowing with a Hamming window
+ * Filter the signal in the spectral domain with a triangular
+ filter-bank, whose filters are approximatively linearly spaced on the
+ mel scale, and have equal bandwidth in the mel scale
+ * Compute the DCT of the log-spectrum
+
+ References
+ ----------
+ .. [1] S.B. Davis and P. Mermelstein, "Comparison of parametric
+ representations for monosyllabic word recognition in continuously
+ spoken sentences", IEEE Trans. Acoustics. Speech, Signal Proc.
+ ASSP-28 (4): 357-366, August 1980."""
+
+ # MFCC parameters: taken from auditory toolbox
+ over = nwin - 160
+ # Pre-emphasis factor (to take into account the -6dB/octave rolloff of the
+ # radiation at the lips level)
+ prefac = 0.97
+
+ # lowfreq = 400 / 3.
+ lowfreq = 133.33
+ # highfreq = 6855.4976
+ linsc = 200 / 3.0
+ logsc = 1.0711703
+
+ nlinfil = 13
+ nlogfil = 27
+ nfil = nlinfil + nlogfil
+
+ w = hamming(nwin, sym=0)
+
+ fbank = trfbank(fs, nfft, lowfreq, linsc, logsc, nlinfil, nlogfil)[0]
+
+ # ------------------
+ # Compute the MFCC
+ # ------------------
+ extract = lfilter([1.0, -prefac], 1, input)
+ framed = segment_axis(extract, nwin, over) * w
+
+ # Compute the spectrum magnitude
+ spec = np.abs(fft(framed, nfft, axis=-1))
+ # Filter the spectrum through the triangle filterbank
+ mspec = np.log10(np.dot(spec, fbank.T))
+ # Use the DCT to 'compress' the coefficients (spectrum -> cepstrum domain)
+ ceps = dct(mspec, type=2, norm="ortho", axis=-1)[:, :nceps]
+
+ return ceps, mspec, spec
diff --git a/power_sequencer/operators/audiosync/mfcc/segment_axis.py b/power_sequencer/operators/audiosync/mfcc/segment_axis.py
new file mode 100644
index 00000000..a8345833
--- /dev/null
+++ b/power_sequencer/operators/audiosync/mfcc/segment_axis.py
@@ -0,0 +1,110 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import numpy as np
+import warnings
+
+
+def segment_axis(a, length, overlap=0, axis=None, end="cut", endvalue=0):
+ """Generate a new array that chops the given array along the given axis
+ into overlapping frames.
+
+ example:
+ >>> segment_axis(arange(10), 4, 2)
+ array([[0, 1, 2, 3],
+ [2, 3, 4, 5],
+ [4, 5, 6, 7],
+ [6, 7, 8, 9]])
+
+ arguments:
+ a The array to segment
+ length The length of each frame
+ overlap The number of array elements by which the frames should overlap
+ axis The axis to operate on; if None, act on the flattened array
+ end What to do with the last frame, if the array is not evenly
+ divisible into pieces. Options are:
+
+ 'cut' Simply discard the extra values
+ 'wrap' Copy values from the beginning of the array
+ 'pad' Pad with a constant value
+
+ endvalue The value to use for end='pad'
+
+ The array is not copied unless necessary (either because it is unevenly
+ strided and being flattened or because end is set to 'pad' or 'wrap').
+ """
+
+ if axis is None:
+ a = np.ravel(a) # may copy
+ axis = 0
+
+ l = a.shape[axis]
+
+ if overlap >= length:
+ raise ValueError("frames cannot overlap by more than 100%")
+ if overlap < 0 or length <= 0:
+ raise ValueError("overlap must be nonnegative and length must " "be positive")
+
+ if l < length or (l - length) % (length - overlap):
+ if l > length:
+ roundup = length + (1 + (l - length) // (length - overlap)) * (length - overlap)
+ rounddown = length + ((l - length) // (length - overlap)) * (length - overlap)
+ else:
+ roundup = length
+ rounddown = 0
+ assert rounddown < l < roundup
+ assert roundup == rounddown + (length - overlap) or (roundup == length and rounddown == 0)
+ a = a.swapaxes(-1, axis)
+
+ if end == "cut":
+ a = a[..., :rounddown]
+ elif end in ["pad", "wrap"]: # copying will be necessary
+ s = list(a.shape)
+ s[-1] = roundup
+ b = np.empty(s, dtype=a.dtype)
+ b[..., :l] = a
+ if end == "pad":
+ b[..., l:] = endvalue
+ elif end == "wrap":
+ b[..., l:] = a[..., : roundup - l]
+ a = b
+
+ a = a.swapaxes(-1, axis)
+
+ l = a.shape[axis]
+ if l == 0:
+ raise ValueError(
+ "Not enough data points to segment array in 'cut' mode; " "try 'pad' or 'wrap'"
+ )
+ assert l >= length
+ assert (l - length) % (length - overlap) == 0
+ n = 1 + (l - length) // (length - overlap)
+ s = a.strides[axis]
+ newshape = a.shape[:axis] + (n, length) + a.shape[axis + 1 :]
+ newstrides = a.strides[:axis] + ((length - overlap) * s, s) + a.strides[axis + 1 :]
+
+ try:
+ return np.ndarray.__new__(
+ np.ndarray, strides=newstrides, shape=newshape, buffer=a, dtype=a.dtype
+ )
+ except TypeError:
+ warnings.warn("Problem with ndarray creation forces copy.")
+ a = a.copy()
+ # Shape doesn't change but strides does
+ newstrides = a.strides[:axis] + ((length - overlap) * s, s) + a.strides[axis + 1 :]
+ return np.ndarray.__new__(
+ np.ndarray, strides=newstrides, shape=newshape, buffer=a, dtype=a.dtype
+ )
diff --git a/power_sequencer/operators/audiosync/mfcc/trfbank.py b/power_sequencer/operators/audiosync/mfcc/trfbank.py
new file mode 100644
index 00000000..00558944
--- /dev/null
+++ b/power_sequencer/operators/audiosync/mfcc/trfbank.py
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import numpy as np
+
+
+def trfbank(fs, nfft, lowfreq, linsc, logsc, nlinfilt, nlogfilt):
+ """Compute triangular filterbank for MFCC computation."""
+ # Total number of filters
+ nfilt = nlinfilt + nlogfilt
+
+ # ------------------------
+ # Compute the filter bank
+ # ------------------------
+ # Compute start/middle/end points of the triangular filters in spectral
+ # domain
+ freqs = np.zeros(nfilt + 2)
+ freqs[:nlinfilt] = lowfreq + np.arange(nlinfilt) * linsc
+ freqs[nlinfilt:] = freqs[nlinfilt - 1] * logsc ** np.arange(1, nlogfilt + 3)
+ heights = 2.0 / (freqs[2:] - freqs[0:-2])
+
+ # Compute filterbank coeff (in fft domain, in bins)
+ fbank = np.zeros((nfilt, nfft))
+ # FFT bins (in Hz)
+ nfreqs = np.arange(nfft) / (1.0 * nfft) * fs
+ for i in range(nfilt):
+ low = freqs[i]
+ cen = freqs[i + 1]
+ hi = freqs[i + 2]
+
+ lid = np.arange(np.floor(low * nfft / fs) + 1, np.floor(cen * nfft / fs) + 1, dtype=np.int)
+ lslope = heights[i] / (cen - low)
+ rid = np.arange(np.floor(cen * nfft / fs) + 1, np.floor(hi * nfft / fs) + 1, dtype=np.int)
+ rslope = heights[i] / (hi - cen)
+ fbank[i][lid] = lslope * (nfreqs[lid] - low)
+ fbank[i][rid] = rslope * (hi - nfreqs[rid])
+
+ return fbank, freqs
diff --git a/power_sequencer/operators/audiosync/std_mfcc.py b/power_sequencer/operators/audiosync/std_mfcc.py
new file mode 100644
index 00000000..3fccae0c
--- /dev/null
+++ b/power_sequencer/operators/audiosync/std_mfcc.py
@@ -0,0 +1,21 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import numpy as np
+
+
+def std_mfcc(mfcc):
+ return (mfcc - np.mean(mfcc, axis=0)) / np.std(mfcc, axis=0)
diff --git a/power_sequencer/operators/channel_offset.py b/power_sequencer/operators/channel_offset.py
new file mode 100644
index 00000000..2779faac
--- /dev/null
+++ b/power_sequencer/operators/channel_offset.py
@@ -0,0 +1,79 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_channel_offset(bpy.types.Operator):
+ """
+ Move selected strip to the nearest open channel above/down
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "UP_ARROW", "value": "PRESS", "alt": True},
+ {"direction": "up"},
+ "Move to Open Channel Above",
+ ),
+ (
+ {"type": "DOWN_ARROW", "value": "PRESS", "alt": True},
+ {"direction": "down"},
+ "Move to Open Channel Below",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ direction: bpy.props.EnumProperty(
+ items=[
+ ("up", "up", "Move the selection 1 channel up"),
+ ("down", "down", "Move the selection 1 channel down"),
+ ],
+ name="Direction",
+ description="Move the sequences up or down",
+ default="up",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ selection = [s for s in context.selected_sequences if not s.lock]
+ if not selection:
+ return {"CANCELLED"}
+
+ selection = sorted(selection, key=attrgetter("channel", "frame_final_start"))
+
+ if self.direction == "up":
+ for s in reversed(selection):
+ s.channel += 1
+ elif self.direction == "down":
+ for s in selection:
+ if s.channel > 1:
+ s.channel -= 1
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/concatenate_strips.py b/power_sequencer/operators/concatenate_strips.py
new file mode 100644
index 00000000..29d11384
--- /dev/null
+++ b/power_sequencer/operators/concatenate_strips.py
@@ -0,0 +1,154 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.global_settings import SequenceTypes
+from .utils.functions import find_sequences_after, get_mouse_frame_and_channel, sequencer_workaround_2_80_audio_bug
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+def find_sequences_before(context, strip):
+ """
+ Returns a list of sequences that are before the strip in the current context
+ """
+ return [s for s in context.sequences if s.frame_final_end <= strip.frame_final_start]
+
+
+class POWER_SEQUENCER_OT_concatenate_strips(bpy.types.Operator):
+ """
+ *brief* Remove space between strips
+
+ Concatenates selected strips in a channel, i.e. removes the gap between them. If a single
+ strip is selected, either the next strip in the channel will be concatenated, or all
+ strips in the channel will be concatenated depending on which shortcut is used.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/YyEL8YP.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "C", "value": "PRESS"},
+ {"concatenate_all": False, "to_left": True},
+ ("Concatenate and select the next strip in the channel"),
+ ),
+ (
+ {"type": "C", "value": "PRESS", "shift": True},
+ {"concatenate_all": True, "to_left": True},
+ "Concatenate all strips in selected channels",
+ ),
+ (
+ {"type": "C", "value": "PRESS", "alt": True},
+ {"concatenate_all": False, "to_left": False},
+ ("Concatenate and select the previous strip in the channel towards the right"),
+ ),
+ (
+ {"type": "C", "value": "PRESS", "shift": True, "alt": True},
+ {"concatenate_all": True, "to_left": False},
+ "Shift Alt C; Concatenate all strips in channel towards the right",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ concatenate_all: bpy.props.BoolProperty(
+ name="Concatenate all strips in channel",
+ description=("If only one strip selected, concatenate" " the entire channel"),
+ default=False,
+ )
+ to_left: bpy.props.BoolProperty(
+ name="To Left",
+ description="Concatenate strips moving them back in time (default) or forward in time",
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences and context.selected_sequences
+
+ def invoke(self, context, event):
+ if not context.selected_sequences:
+ frame, channel = get_mouse_frame_and_channel(context, event)
+ bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel)
+ return self.execute(context)
+
+ def execute(self, context):
+ selection = context.selected_sequences
+ channels = {s.channel for s in selection}
+
+ if len(selection) == len(channels):
+ for s in selection:
+ candidates = (
+ find_sequences_before(context, s)
+ if not self.to_left
+ else find_sequences_after(context, s)
+ )
+ to_concatenate = [
+ strip
+ for strip in candidates
+ if strip.channel == s.channel
+ and not strip.lock
+ and strip.type in SequenceTypes.CUTABLE
+ ]
+ self.concatenate(s, to_concatenate)
+
+ else:
+ for channel in channels:
+ to_concatenate = [s for s in selection if s.channel == channel]
+ strip_target = (
+ min(to_concatenate, key=lambda s: s.frame_final_start)
+ if self.to_left
+ else max(to_concatenate, key=lambda s: s.frame_final_start)
+ )
+ to_concatenate.remove(strip_target)
+ self.concatenate(strip_target, to_concatenate, force_all=True)
+
+ sequencer_workaround_2_80_audio_bug(context)
+ return {"FINISHED"}
+
+ def concatenate(self, strip_target, sequences, force_all=False):
+ to_concatenate = sorted(sequences, key=attrgetter("frame_final_start"))
+ to_concatenate = list(reversed(to_concatenate)) if not self.to_left else to_concatenate
+ to_concatenate = (
+ [to_concatenate[0]] if not (self.concatenate_all or force_all) else to_concatenate
+ )
+
+ attribute_target = "frame_final_end" if self.to_left else "frame_final_start"
+ attribute_concat = "frame_final_start" if self.to_left else "frame_final_end"
+ concatenate_start = getattr(strip_target, attribute_target)
+ last_gap = 0
+ for s in to_concatenate:
+ if isinstance(s, bpy.types.EffectSequence):
+ concatenate_start = (
+ s.frame_final_end - last_gap if self.to_left else s.frame_final_start - last_gap
+ )
+ continue
+ concat_strip_frame = getattr(s, attribute_concat)
+ gap = concat_strip_frame - concatenate_start
+ s.frame_start -= gap
+ concatenate_start = s.frame_final_end if self.to_left else s.frame_final_start
+ last_gap = gap
+
+ if not self.concatenate_all:
+ strip_target.select = False
+ to_concatenate[0].select = True
diff --git a/power_sequencer/operators/copy_selected_sequences.py b/power_sequencer/operators/copy_selected_sequences.py
new file mode 100644
index 00000000..69f66714
--- /dev/null
+++ b/power_sequencer/operators/copy_selected_sequences.py
@@ -0,0 +1,95 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_copy_selected_sequences(bpy.types.Operator):
+ """
+ *brief* Copy/cut strips without offset from current time indicator
+
+
+ Copies the selected sequences without frame offset and optionally
+ deletes the selection to give a cut to clipboard effect. This
+ operator overrides the default Blender copy method which includes
+ cursor offset when pasting, which is atypical of copy/paste methods.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/w6z1Jb1.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "C", "value": "PRESS", "ctrl": True},
+ {"delete_selection": False},
+ "Copy Selected Strips",
+ ),
+ (
+ {"type": "X", "value": "PRESS", "ctrl": True},
+ {"delete_selection": True},
+ "Cut Selected Strips",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ delete_selection: bpy.props.BoolProperty(
+ name="Delete selection",
+ description="Delete selected strips: acts like cut and paste",
+ default=False,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ cursor_start_frame = context.scene.frame_current
+ sequencer = bpy.ops.sequencer
+
+ # Deactivate audio playback and video preview
+ scene = context.scene
+ initial_audio_setting = scene.use_audio_scrub
+ initial_proxy_size = context.space_data.proxy_render_size
+ scene.use_audio_scrub = False
+ context.space_data.proxy_render_size = "NONE"
+
+ first_sequence = min(context.selected_sequences, key=attrgetter("frame_final_start"))
+ context.scene.frame_current = first_sequence.frame_final_start
+ sequencer.copy()
+ context.scene.frame_current = cursor_start_frame
+
+ scene.use_audio_scrub = initial_audio_setting
+ context.space_data.proxy_render_size = initial_proxy_size
+
+ if self.delete_selection:
+ sequencer.delete()
+
+ plural_string = "s" if len(context.selected_sequences) != 1 else ""
+ action_verb = "Cut" if self.delete_selection else "Copied"
+ report_message = "{!s} {!s} sequence{!s} to the clipboard.".format(
+ action_verb, str(len(context.selected_sequences)), plural_string
+ )
+ self.report({"INFO"}, report_message)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/crossfade_add.py b/power_sequencer/operators/crossfade_add.py
new file mode 100644
index 00000000..31f97af2
--- /dev/null
+++ b/power_sequencer/operators/crossfade_add.py
@@ -0,0 +1,119 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import find_sequences_after
+from .utils.functions import convert_duration_to_frames
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_crossfade_add(bpy.types.Operator):
+ """
+ *brief* Adds cross fade between selected sequence and the closest sequence to its right
+
+
+ Based on the active strip, finds the closest next sequence of a similar type, moves it
+ so it overlaps the active strip, and adds a gamma cross effect between them. Works with
+ MOVIE, IMAGE and META strips
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/ZyEd0jD.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "C", "value": "PRESS", "ctrl": True, "alt": True}, {}, "Add Crossfade")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ crossfade_duration: bpy.props.FloatProperty(
+ name="Crossfade Duration", description="The duration of the crossfade", default=0.5, min=0
+ )
+ auto_move_strip: bpy.props.BoolProperty(
+ name="Auto Move Strip",
+ description=(
+ "When true, moves the second strip so the crossfade"
+ " is of the length set in 'Crossfade Length'"
+ ),
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ sorted_selection = sorted(context.selected_sequences, key=lambda s: s.frame_final_start)
+ for s in sorted_selection:
+ s_next = self.get_next_sequence_after(context, s)
+ s_to_offset = s_next if s_next.type not in SequenceTypes.EFFECT else s_next.input_1
+
+ if self.auto_move_strip:
+ offset = s_to_offset.frame_final_start - s.frame_final_end
+ s_to_offset.frame_start -= offset
+
+ if s_to_offset.frame_final_start == s.frame_final_end:
+ self.offset_sequence_handles(context, s, s_to_offset)
+
+ self.apply_crossfade(context, s, s_next)
+ return {"FINISHED"}
+
+ def get_next_sequence_after(self, context, sequence):
+ """
+ Returns the first sequence after `sequence` by frame_final_start
+ """
+ next_sequence = None
+ next_in_channel = [
+ s for s in find_sequences_after(context, sequence) if s.channel == sequence.channel
+ ]
+ next_transitionable = (s for s in next_in_channel if s.type in SequenceTypes.TRANSITIONABLE)
+ try:
+ next_sequence = min(next_transitionable, key=lambda s: s.frame_final_start)
+ except ValueError:
+ pass
+ return next_sequence
+
+ def apply_crossfade(self, context, strip_from, strip_to):
+ for s in bpy.context.selected_sequences:
+ s.select = False
+ strip_from.select = True
+ strip_to.select = True
+ context.scene.sequence_editor.active_strip = strip_to
+ bpy.ops.sequencer.effect_strip_add(type="GAMMA_CROSS")
+
+ def offset_sequence_handles(self, context, sequence_1, sequence_2):
+ """
+ Moves the handles of the two sequences before adding the crossfade
+ """
+ fade_duration = convert_duration_to_frames(context, self.crossfade_duration)
+ fade_offset = fade_duration / 2
+
+ if hasattr(sequence_1, "input_1"):
+ sequence_1.input_1.frame_final_end -= fade_offset
+ else:
+ sequence_1.frame_final_end -= fade_offset
+
+ if hasattr(sequence_2, "input_1"):
+ sequence_2.input_1.frame_final_start += fade_offset
+ else:
+ sequence_2.frame_final_start += fade_offset
diff --git a/power_sequencer/operators/crossfade_edit.py b/power_sequencer/operators/crossfade_edit.py
new file mode 100644
index 00000000..127ab603
--- /dev/null
+++ b/power_sequencer/operators/crossfade_edit.py
@@ -0,0 +1,89 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_crossfade_edit(bpy.types.Operator):
+ """
+ *brief* Adjust the location of the crossfade between 2 strips
+
+
+ Selects the handles of both inputs of a crossfade strip's input and
+ calls the grab operator. Allows you to quickly change the location
+ of a fade transition between two strips.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/rCmLhg6.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ crossfade_types = ["CROSS", "GAMMA_CROSS"]
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor.active_strip and context.selected_sequences
+
+
+ def execute(self, context):
+ active = context.scene.sequence_editor.active_strip
+ if active.type not in self.crossfade_types:
+ effect = self.find_cross_effect(context, active)
+ if not effect:
+ return {"CANCELLED"}
+ active = context.scene.sequence_editor.active_strip = effect
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ active.select = True
+ active.input_1.select_right_handle = True
+ active.input_2.select_left_handle = True
+ active.input_1.select = True
+ active.input_2.select = True
+ bpy.ops.transform.seq_slide("INVOKE_DEFAULT")
+ return {"FINISHED"}
+
+ def find_cross_effect(self, sequence):
+ """
+ Takes a single strip and finds effect strips that use it as input
+ Returns the effect strip(s) found as a list, ordered by starting frame
+ Returns None if no effect was found
+ """
+ if sequence.type not in SequenceTypes.VIDEO + SequenceTypes.IMAGE:
+ return
+
+ effect_sequences = (s for s in context.sequences if s.type in SequenceTypes.EFFECT)
+ found_effect_strips = []
+ for s in effect_sequences:
+ if s.input_1.name == sequence.name:
+ found_effect_strips.append(s)
+ if s.input_count == 2:
+ if s.input_2.name == sequence.name:
+ found_effect_strips.append(s)
+ for e in found_effect_strips:
+ if e.type not in self.crossfade_types:
+ continue
+ return e
diff --git a/power_sequencer/operators/cut_strips_under_cursor.py b/power_sequencer/operators/cut_strips_under_cursor.py
new file mode 100644
index 00000000..de1fff3e
--- /dev/null
+++ b/power_sequencer/operators/cut_strips_under_cursor.py
@@ -0,0 +1,65 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+from .utils.functions import get_mouse_frame_and_channel
+
+
+class POWER_SEQUENCER_OT_split_strips_under_cursor(bpy.types.Operator):
+ """
+ Splits all strips under cursor including muted strips, but excluding locked strips.
+ Auto selects sequences under the time cursor when you don't have a selection.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/ZyEd0jD.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "K", "value": "PRESS"}, {}, "Cut All Strips Under Cursor")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ side: bpy.props.EnumProperty(
+ items=[("LEFT", "", ""), ("RIGHT", "", "")], name="Side", default="LEFT", options={"HIDDEN"}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ frame, channel = get_mouse_frame_and_channel(context, event)
+ self.side = "LEFT" if frame < context.scene.frame_current else "RIGHT"
+ return self.execute(context)
+
+ def execute(self, context):
+ # Deselect to trigger a call to select_strips_under_cursor below if the
+ # time cursor doesn't overlap any of the selected strip: if so, it
+ # can't cut anything!
+ deselect = True
+ for s in bpy.context.selected_sequences:
+ if s.frame_final_start <= context.scene.frame_current <= s.frame_final_end:
+ deselect = False
+ if deselect:
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ (context.selected_sequences or bpy.ops.power_sequencer.select_strips_under_cursor())
+ return bpy.ops.sequencer.cut(frame=context.scene.frame_current, side=self.side)
diff --git a/power_sequencer/operators/delete_direct.py b/power_sequencer/operators/delete_direct.py
new file mode 100644
index 00000000..2650ddb1
--- /dev/null
+++ b/power_sequencer/operators/delete_direct.py
@@ -0,0 +1,74 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_mouse_frame_and_channel
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_delete_direct(bpy.types.Operator):
+ """
+ Deletes strips without confirmation, and cleans up crossfades nicely.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "X", "value": "PRESS"}, {}, "Delete Direct"),
+ (
+ {"type": "X", "alt": True, "value": "PRESS"},
+ {"is_removing_transitions": True},
+ "Delete Direct with Transitions",
+ ),
+ ({"type": "DEL", "value": "PRESS"}, {}, "Delete Direct"),
+ (
+ {"type": "DEL", "alt": True, "value": "PRESS"},
+ {"is_removing_transitions": True},
+ "Delete Direct with Transitions",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ is_removing_transitions: bpy.props.BoolProperty(name="Remove Transitions", default=False)
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def invoke(self, context, event):
+ frame, channel = get_mouse_frame_and_channel(context, event)
+ if not context.selected_sequences:
+ bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel)
+ return self.execute(context)
+
+ def execute(self, context):
+ selection = context.selected_sequences
+ if self.is_removing_transitions and bpy.ops.power_sequencer.transitions_remove.poll():
+ bpy.ops.power_sequencer.transitions_remove()
+ bpy.ops.sequencer.delete()
+
+ report_message = "Deleted " + str(len(selection)) + " sequence"
+ report_message += "s" if len(selection) > 1 else ""
+ self.report({"INFO"}, report_message)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/deselect_all_left_or_right.py b/power_sequencer/operators/deselect_all_left_or_right.py
new file mode 100644
index 00000000..c250095d
--- /dev/null
+++ b/power_sequencer/operators/deselect_all_left_or_right.py
@@ -0,0 +1,86 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_deselect_all_strips_left_or_right(bpy.types.Operator):
+ """
+ Deselects all the strips at the left or right of the time cursor, based on the position
+ of the mouse
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "Q", "value": "PRESS", "alt": True},
+ {"side": "left"},
+ "Deselect all strips to the left of the time cursor",
+ ),
+ (
+ {"type": "E", "value": "PRESS", "alt": True},
+ {"side": "right"},
+ "Deselect all strips to the right of the time cursor",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ side: bpy.props.EnumProperty(
+ name="Side",
+ description="The side to deselect",
+ items=[
+ (
+ "mouse",
+ "Mouse position",
+ ("Deselect based on the mouse position relative to the" " time cursor"),
+ ),
+ ("left", "Left", "Left of the time cursor"),
+ ("right", "Right", "Right of the time cursor"),
+ ],
+ default="mouse",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def invoke(self, context, event):
+ frame_current = context.scene.frame_current
+ frame_mouse = context.region.view2d.region_to_view(event.mouse_region_x, 1)[0]
+
+ for s in context.sequences:
+ if self.side == "left" or frame_mouse < frame_current and self.side == "mouse":
+ if s.frame_final_end < frame_current:
+ self.deselect(s)
+ elif self.side == "right" or frame_mouse >= frame_current and self.side == "mouse":
+ if s.frame_final_start >= frame_current:
+ self.deselect(s)
+ return {"FINISHED"}
+
+ def deselect(self, strip):
+ strip.select = False
+ strip.select_left_handle = False
+ strip.select_right_handle = False
diff --git a/power_sequencer/operators/deselect_handles_and_grab.py b/power_sequencer/operators/deselect_handles_and_grab.py
new file mode 100644
index 00000000..78365906
--- /dev/null
+++ b/power_sequencer/operators/deselect_handles_and_grab.py
@@ -0,0 +1,50 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_deselect_handles_and_grab(bpy.types.Operator):
+ """
+ Deselect the handles of all selected strips and call the Sequence Slide operator
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ for s in context.selected_sequences:
+ s.select_left_handle = False
+ s.select_right_handle = False
+ s.select = True
+
+ bpy.ops.transform.seq_slide("INVOKE_DEFAULT")
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/duplicate_move.py b/power_sequencer/operators/duplicate_move.py
new file mode 100644
index 00000000..8a72e28e
--- /dev/null
+++ b/power_sequencer/operators/duplicate_move.py
@@ -0,0 +1,56 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_mouse_frame_and_channel
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_duplicate_move(bpy.types.Operator):
+ """
+ Auto selects the strip under the mouse if nothing is selected, and calls Blender's
+ Duplicate Move function
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "D", "value": "PRESS"}, {}, "Duplicate Move"),
+ ({"type": "D", "value": "PRESS", "shift": True}, {}, "Duplicate Move"),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ frame, channel = get_mouse_frame_and_channel(context, event)
+ if not context.selected_sequences:
+ bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel)
+ return self.execute(context)
+
+ def execute(self, context):
+ bpy.ops.sequencer.duplicate_move("INVOKE_DEFAULT")
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/expand_to_surrounding_cuts.py b/power_sequencer/operators/expand_to_surrounding_cuts.py
new file mode 100644
index 00000000..34507da8
--- /dev/null
+++ b/power_sequencer/operators/expand_to_surrounding_cuts.py
@@ -0,0 +1,104 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from math import floor
+
+from .utils.functions import convert_duration_to_frames
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+from .utils.functions import slice_selection
+
+
+class POWER_SEQUENCER_OT_expand_to_surrounding_cuts(bpy.types.Operator):
+ """
+ *Brief* Expand selected strips to surrounding cuts
+
+ Finds potential gaps surrounding each block of selected sequences and extends the corresponding
+ sequence handle to it.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "E", "value": "PRESS", "ctrl": True}, {}, "Expand to Surrounding Cuts")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ margin: bpy.props.FloatProperty(
+ name="Trim margin",
+ description="Margin to leave on either sides of the trim in seconds",
+ default=0.2,
+ min=0,
+ )
+ gap_remove: bpy.props.BoolProperty(
+ name="Remove gaps",
+ description="When trimming the sequences, remove gaps automatically",
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def invoke(self, context, event):
+ sequence_blocks = slice_selection(context, context.selected_sequences)
+ for sequences in sequence_blocks:
+ sequences_frame_start = min(sequences, key=lambda s: s.frame_final_start).frame_final_start
+ sequences_frame_end = max(sequences, key=lambda s: s.frame_final_end).frame_final_end
+
+ frame_left, frame_right = find_closest_cuts(
+ context, sequences_frame_start, sequences_frame_end
+ )
+ if (
+ sequences_frame_start == frame_left
+ and sequences_frame_end == frame_right
+ ):
+ continue
+
+ to_extend_left = [s for s in sequences if s.frame_final_start == sequences_frame_start]
+ to_extend_right = [s for s in sequences if s.frame_final_end == sequences_frame_end]
+
+ for s in to_extend_left:
+ s.frame_final_start = (
+ frame_left
+ if frame_left < sequences_frame_start
+ else sequences_frame_start
+ )
+ for s in to_extend_right:
+ s.frame_final_end = (
+ frame_right
+ if frame_right > sequences_frame_end
+ else sequences_frame_end
+ )
+ return {"FINISHED"}
+
+
+def find_closest_cuts(context, frame_min, frame_max):
+ frame_left = max(
+ context.sequences, key=lambda s: s.frame_final_end if s.frame_final_end <= frame_min else -1
+ ).frame_final_end
+ frame_right = min(
+ context.sequences,
+ key=lambda s: s.frame_final_start if s.frame_final_start >= frame_max else 1000000,
+ ).frame_final_start
+ return frame_left, frame_right
diff --git a/power_sequencer/operators/fade_add.py b/power_sequencer/operators/fade_add.py
new file mode 100644
index 00000000..d72f034d
--- /dev/null
+++ b/power_sequencer/operators/fade_add.py
@@ -0,0 +1,254 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from mathutils import Vector
+from math import floor
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_fade_add(bpy.types.Operator):
+ """*brief* Adds or updates a fade animation for either visual or audio strips.
+
+ Fade options:
+
+ - In, Out, In and Out create a fade animation of the given duration from
+ the start of the sequence, to the end of the sequence, or on boths sides
+ - From playhead: the fade animation goes from the start of sequences under the playhead to the playhead
+ - To playhead: the fade animation goes from the playhead to the end of sequences under the playhead
+
+ By default, the duration of the fade is 1 second.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/XoUM2vw.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "F", "value": "PRESS", "alt": True}, {"type": "OUT"}, "Fade Out"),
+ ({"type": "F", "value": "PRESS", "ctrl": True}, {"type": "IN"}, "Fade In"),
+ ({"type": "F", "value": "PRESS"}, {"type": "IN_OUT"}, "Fade In and Out"),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ duration_seconds: bpy.props.FloatProperty(
+ name="Fade Duration", description="Duration of the fade in seconds", default=1.0, min=0.01
+ )
+ type: bpy.props.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 playhead",
+ "Fade from the time cursor to the end of overlapping sequences",
+ ),
+ (
+ "CURSOR_TO",
+ "To playhead",
+ "Fade from the start of sequences under the time cursor to the current frame",
+ ),
+ ],
+ name="Fade type",
+ description="Fade in, out, or both in and out. Default is both",
+ default="IN_OUT",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ # 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
+
+ sequences = context.selected_sequences
+ if self.type in ["CURSOR_TO", "CURSOR_FROM"]:
+ sequences = [
+ s
+ for s in sequences
+ if s.frame_final_start < context.scene.frame_current < s.frame_final_end
+ ]
+
+ max_duration = min(sequences, key=lambda s: s.frame_final_duration).frame_final_duration
+ max_duration = floor(max_duration / 2.0) if self.type == "IN_OUT" else max_duration
+
+ faded_sequences = []
+ for sequence in sequences:
+ duration = self.calculate_fade_duration(context, sequence)
+ duration = min(duration, max_duration)
+
+ if not self.is_long_enough(sequence, duration):
+ continue
+
+ animated_property = "volume" if hasattr(sequence, "volume") else "blend_alpha"
+ fade_fcurve = fade_find_or_create_fcurve(context, sequence, animated_property)
+ fades = self.calculate_fades(sequence, fade_fcurve, animated_property, duration)
+ fade_animation_clear(context, fade_fcurve, fades)
+ fade_animation_create(fade_fcurve, fades)
+ faded_sequences.append(sequence)
+
+ sequence_string = "sequence" if len(faded_sequences) == 1 else "sequences"
+ self.report(
+ {"INFO"}, "Added fade animation to {} {}.".format(len(faded_sequences), sequence_string)
+ )
+ return {"FINISHED"}
+
+ def calculate_fade_duration(self, context, sequence):
+ frame_current = context.scene.frame_current
+ duration = 0.0
+ if self.type == "CURSOR_TO":
+ duration = abs(frame_current - sequence.frame_final_start)
+ elif self.type == "CURSOR_FROM":
+ duration = abs(sequence.frame_final_end - frame_current)
+ else:
+ duration = calculate_duration_frames(context, self.duration_seconds)
+ return max(1, duration)
+
+ def is_long_enough(self, sequence, duration=0.0):
+ minimum_duration = duration * 2 if self.type == "IN_OUT" else duration
+ return sequence.frame_final_duration >= minimum_duration
+
+ def calculate_fades(self, sequence, fade_fcurve, animated_property, duration):
+ """
+ Returns a list of Fade objects
+ """
+ fades = []
+ if self.type in ["IN", "IN_OUT", "CURSOR_TO"]:
+ fade = Fade(sequence, fade_fcurve, "IN", animated_property, duration)
+ fades.append(fade)
+ if self.type in ["OUT", "IN_OUT", "CURSOR_FROM"]:
+ fade = Fade(sequence, fade_fcurve, "OUT", animated_property, duration)
+ fades.append(fade)
+ return fades
+
+
+def fade_find_or_create_fcurve(context, sequence, animated_property):
+ """
+ Iterates over all the fcurves until it finds an fcurve with a data path
+ that corresponds to the sequence.
+ Returns the matching FCurve or creates a new one if the function can't find a match.
+ """
+ fade_fcurve = None
+ fcurves = context.scene.animation_data.action.fcurves
+ searched_data_path = sequence.path_from_id(animated_property)
+ for fcurve in fcurves:
+ if fcurve.data_path == searched_data_path:
+ fade_fcurve = fcurve
+ break
+ if not fade_fcurve:
+ fade_fcurve = fcurves.new(data_path=searched_data_path)
+ return fade_fcurve
+
+
+def fade_animation_clear(context, 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 keyframe in keyframe_points:
+ for fade in fades:
+ # 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 ReferenceError:
+ pass
+
+
+def fade_animation_create(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 waveforms only redraw upon "moving" a keyframe
+ keyframe_points[-1].co = keyframe_points[-1].co
+
+
+class Fade:
+ """
+ Data structure to represent fades
+ """
+
+ type = ""
+ animated_property = ""
+ duration = -1
+ max_value = 1.0
+ start, end = Vector((0, 0)), Vector((0, 0))
+
+ def __init__(self, sequence, fade_fcurve, type, animated_property, duration):
+ self.type = type
+ self.animated_property = animated_property
+ self.duration = duration
+ self.max_value = self.calculate_max_value(sequence, fade_fcurve)
+
+ if type == "IN":
+ self.start = Vector((sequence.frame_final_start, 0.0))
+ self.end = Vector((sequence.frame_final_start + self.duration, self.max_value))
+ elif type == "OUT":
+ self.start = Vector((sequence.frame_final_end - self.duration, self.max_value))
+ self.end = Vector((sequence.frame_final_end, 0.0))
+
+ def calculate_max_value(self, sequence, fade_fcurve):
+ """
+ Returns the maximum Y coordinate the fade animation should use for a given sequence
+ Uses either the sequence'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(sequence, self.animated_property, 1.0)
+ else:
+ if self.type == "IN":
+ fade_end = sequence.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 = sequence.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 {}: {} to {}".format(self.type, self.start, self.end)
+
+
+def calculate_duration_frames(context, duration_seconds):
+ return round(duration_seconds * context.scene.render.fps / context.scene.render.fps_base)
diff --git a/power_sequencer/operators/fade_clear.py b/power_sequencer/operators/fade_clear.py
new file mode 100644
index 00000000..d7abb4d7
--- /dev/null
+++ b/power_sequencer/operators/fade_clear.py
@@ -0,0 +1,64 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+from .utils.global_settings import SequenceTypes
+
+
+class POWER_SEQUENCER_OT_fade_clear(bpy.types.Operator):
+ """
+ *brief* Removes fade animation from selected sequences.
+
+ Removes opacity or volume animation on selected sequences and resets the
+ property to a value of 1.0. Works on all types of sequences.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "F", "value": "PRESS", "alt": True, "ctrl": True}, {}, "Clear Fades")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ fcurves = context.scene.animation_data.action.fcurves
+
+ for sequence in context.selected_sequences:
+ animated_property = "volume" if hasattr(sequence, "volume") else "blend_alpha"
+ for curve in fcurves:
+ if not curve.data_path.endswith(animated_property):
+ continue
+ # Ensure the fcurve corresponds to the selected sequence
+ if sequence == eval(
+ "bpy.context.scene." + curve.data_path.replace("." + animated_property, "")
+ ):
+ fcurves.remove(curve)
+ setattr(sequence, animated_property, 1.0)
+
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/gap_remove.py b/power_sequencer/operators/gap_remove.py
new file mode 100644
index 00000000..8ef00b6b
--- /dev/null
+++ b/power_sequencer/operators/gap_remove.py
@@ -0,0 +1,135 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.functions import slice_selection, sequencer_workaround_2_80_audio_bug
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_gap_remove(bpy.types.Operator):
+ """
+ Remove gaps, starting from the first frame, with the ability to ignore locked strips
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ ignore_locked: bpy.props.BoolProperty(
+ name="Ignore Locked Strips",
+ description="Remove gaps without moving locked strips",
+ default=True,
+ )
+ all: bpy.props.BoolProperty(
+ name="Remove All",
+ description="Remove all gaps starting from the time cursor",
+ default=False,
+ )
+ frame: bpy.props.IntProperty(
+ name="Frame",
+ description="Frame to remove gaps from, defaults at the time cursor",
+ default=-1,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ frame = self.frame if self.frame >= 0 else context.scene.frame_current
+ sequences = (
+ [s for s in context.sequences if not s.lock]
+ if self.ignore_locked
+ else context.sequences
+ )
+ sequences = [
+ s for s in sequences if s.frame_final_start >= frame or s.frame_final_end > frame
+ ]
+ sequence_blocks = slice_selection(context, sequences)
+ if not sequence_blocks:
+ return {"FINISHED"}
+
+ gap_frame = self.find_gap_frame(context, frame, sequence_blocks[0])
+ if gap_frame == -1:
+ return {"FINISHED"}
+
+ first_block_start = min(
+ sequence_blocks[0], key=attrgetter("frame_final_start")
+ ).frame_final_start
+ blocks_after_gap = (
+ sequence_blocks[1:] if first_block_start <= gap_frame else sequence_blocks
+ )
+
+ self.gaps_remove(context, blocks_after_gap, gap_frame)
+ sequencer_workaround_2_80_audio_bug(context)
+ return {"FINISHED"}
+
+ def find_gap_frame(self, context, frame, sorted_sequences):
+ """
+ Takes a list sequences sorted by frame_final_start
+ """
+ strips_start = min(sorted_sequences, key=attrgetter("frame_final_start")).frame_final_start
+ strips_end = max(sorted_sequences, key=attrgetter("frame_final_end")).frame_final_end
+
+ gap_frame = -1
+ if strips_start > frame:
+ strips_before_frame_start = [s for s in context.sequences if s.frame_final_end <= frame]
+ frame_target = 0
+ if strips_before_frame_start:
+ frame_target = max(
+ strips_before_frame_start, key=attrgetter("frame_final_end")
+ ).frame_final_end
+ gap_frame = frame_target if frame_target < strips_start else frame
+ else:
+ gap_frame = strips_end
+ return gap_frame
+
+ def gaps_remove(self, context, sequence_blocks, gap_frame_start):
+ """
+ Recursively removes gaps between blocks of sequences
+ """
+
+ gap_frame = gap_frame_start
+ for block in sequence_blocks:
+ gap_size = block[0].frame_final_start - gap_frame
+ if gap_size < 1:
+ continue
+
+ for s in block:
+ try:
+ s.frame_start -= gap_size
+ except AttributeError:
+ continue
+
+ self.move_markers(context, gap_frame, gap_size)
+ if not self.all:
+ break
+ gap_frame = block[-1].frame_final_end
+
+ def move_markers(self, context, gap_frame, gap_size):
+ markers = (m for m in context.scene.timeline_markers if m.frame > gap_frame)
+ for m in markers:
+ m.frame -= min({gap_size, m.frame - gap_frame})
diff --git a/power_sequencer/operators/grab.py b/power_sequencer/operators/grab.py
new file mode 100644
index 00000000..af8109ff
--- /dev/null
+++ b/power_sequencer/operators/grab.py
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_mouse_frame_and_channel
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_grab(bpy.types.Operator):
+ """
+ *brief* Grab and move sequences. Extends Blender's built-in grab tool
+
+
+ Grab and move sequences. If you have no strips selected, it automatically
+ finds the strip closest to the mouse and selects it. If you only select
+ one or multiple crossfades, selects the handles on either side of the
+ crossfades before moving sequences, using POWER_SEQUENCER_OT_crossfade_edit
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "G", "value": "PRESS"}, {}, "")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def invoke(self, context, event):
+ frame, channel = get_mouse_frame_and_channel(context, event)
+ if not context.selected_sequences:
+ bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel)
+ return self.execute(context)
+
+ def execute(self, context):
+ if len(context.selected_sequences) == 0:
+ return {'FINISHED'}
+
+ strip = context.selected_sequences[0]
+ if len(context.selected_sequences) == 1 and strip.type in SequenceTypes.TRANSITION:
+ context.scene.sequence_editor.active_strip = strip
+ return bpy.ops.power_sequencer.crossfade_edit()
+ else:
+ return bpy.ops.transform.seq_slide('INVOKE_DEFAULT')
diff --git a/power_sequencer/operators/grab_closest_handle_or_cut.py b/power_sequencer/operators/grab_closest_handle_or_cut.py
new file mode 100644
index 00000000..7d772d79
--- /dev/null
+++ b/power_sequencer/operators/grab_closest_handle_or_cut.py
@@ -0,0 +1,110 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Selects and grabs the strip handle or cut closest to the mouse cursor.
+Hover near a cut and use this operator to slide it.
+"""
+import bpy
+
+from math import floor
+
+from .utils.functions import calculate_distance
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_grab_closest_cut(bpy.types.Operator):
+ """
+ *brief* Grab the handles that form the closest cut
+
+
+ Selects and grabs the strip handle or cut closest to the mouse cursor.
+ Hover near a cut and fire this tool to slide it.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "G", "value": "PRESS", "shift": True, "alt": True},
+ {},
+ "Grab closest handle or cut",
+ )
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ select_linked: bpy.props.BoolProperty(
+ name="Select Linked", description="Select strips that are linked in time", default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ sequencer = bpy.ops.sequencer
+
+ mouse_x, mouse_y = event.mouse_region_x, event.mouse_region_y
+ frame, channel = self.find_cut_closest_to_mouse(context, mouse_x, mouse_y)
+
+ matching_strips = [
+ s
+ for s in context.sequences
+ if (abs(s.frame_final_start - frame) <= 1 or abs(s.frame_final_end - frame) <= 1)
+ ]
+ if not self.select_linked:
+ matching_strips = [s for s in matching_strips if s.channel == channel]
+ sequencer.select_all(action="DESELECT")
+ for s in matching_strips:
+ s.select = True
+ return bpy.ops.power_sequencer.grab_sequence_handles(frame=frame)
+
+ def find_cut_closest_to_mouse(self, context, mouse_x, mouse_y):
+ """
+ Takes the mouse's coordinates in the sequencer area and returns the two
+ strips who share the cut closest to the mouse. Use it to find the
+ handle(s) to select with the grab on the fly operator
+ """
+ view2d = context.region.view2d
+
+ closest_cut = (None, None)
+ distance_to_closest_cut = 1000000.0
+
+ for s in context.sequences:
+ channel_offset = s.channel + 0.5
+ start_x, start_y = view2d.view_to_region(s.frame_final_start, channel_offset)
+ end_x, end_y = view2d.view_to_region(s.frame_final_start, channel_offset)
+
+ distance_to_start = calculate_distance(start_x, start_y, mouse_x, mouse_y)
+ distance_to_end = calculate_distance(end_x, end_y, mouse_x, mouse_y)
+
+ if distance_to_start < distance_to_closest_cut:
+ closest_cut = (start_x, start_y)
+ distance_to_closest_cut = distance_to_start
+ if distance_to_end < distance_to_closest_cut:
+ closest_cut = (end_x, end_y)
+ distance_to_closest_cut = distance_to_end
+
+ closest_cut_local_coords = view2d.region_to_view(closest_cut[0], closest_cut[1])
+ frame, channel = (round(closest_cut_local_coords[0]), floor(closest_cut_local_coords[1]))
+ return frame, channel
diff --git a/power_sequencer/operators/grab_sequence_handles.py b/power_sequencer/operators/grab_sequence_handles.py
new file mode 100644
index 00000000..e2a38ed1
--- /dev/null
+++ b/power_sequencer/operators/grab_sequence_handles.py
@@ -0,0 +1,88 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_grab_sequence_handles(bpy.types.Operator):
+ """
+ *brief* Grabs the sequence's handle based on the mouse position
+
+
+ Extends the sequence based on the mouse position. If the cursor is to the
+ right of the sequence's middle, it moves the right handle. If it's on the
+ left side, it moves the left handle.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "G", "value": "PRESS", "shift": True}, {}, "Grab sequence handles")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ always_find_closest: bpy.props.BoolProperty(name="Always find closest", default=False)
+ frame: bpy.props.IntProperty(name="Frame", default=-1, options={"HIDDEN"})
+ channel: bpy.props.IntProperty(name="Channel", default=-1, options={"HIDDEN"})
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ self.frame, self.channel = context.region.view2d.region_to_view(
+ x=event.mouse_region_x, y=event.mouse_region_y
+ )
+ return self.execute(context)
+
+ def execute(self, context):
+ selection = context.selected_sequences
+ if self.always_find_closest or not selection:
+ if self.frame == -1:
+ return {"CANCELLED"}
+ bpy.ops.power_sequencer.select_closest_to_mouse(frame=self.frame, channel=self.channel)
+ for s in context.selected_sequences:
+ self.select_closest_handle(s)
+ else:
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in selection:
+ if s.type in SequenceTypes.EFFECT and not s.type == "COLOR":
+ self.select_closest_handle(s.input_1)
+ try:
+ self.select_closest_handle(s.input_2)
+ except AttributeError:
+ pass
+ else:
+ self.select_closest_handle(s)
+ return bpy.ops.transform.seq_slide("INVOKE_DEFAULT")
+
+ def select_closest_handle(self, sequence):
+ middle = sequence.frame_final_start + sequence.frame_final_duration / 2
+ if self.frame >= middle:
+ sequence.select_right_handle = True
+ else:
+ sequence.select_left_handle = True
+ sequence.select = True
diff --git a/power_sequencer/operators/import_local_footage.py b/power_sequencer/operators/import_local_footage.py
new file mode 100644
index 00000000..8c8ca1ec
--- /dev/null
+++ b/power_sequencer/operators/import_local_footage.py
@@ -0,0 +1,268 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import json
+import os
+import re
+from operator import attrgetter
+
+import bpy
+
+from .utils.functions import convert_duration_to_frames
+from .utils.doc import doc_brief, doc_description, doc_idname, doc_name
+from ..addon_preferences import get_preferences
+from .utils.global_settings import (
+ Extensions,
+ EXTENSIONS_ALL,
+ EXTENSIONS_AUDIO,
+ EXTENSIONS_IMG,
+ EXTENSIONS_VIDEO,
+)
+
+
+class POWER_SEQUENCER_OT_import_local_footage(bpy.types.Operator):
+ """*brief* Imports video, images, and audio from the project folder
+
+ Finds and imports all valid video, audio files, and pictures in the blend file's folder and
+ sub-folders, ignoring folders named BL_proxy.
+
+ If you set it in the add-on preferences, it also sets imported sequences to use proxies. See
+ `Preferences -> Add-ons -> Blender Power Sequencer -> Proxy`
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "I", "value": "PRESS", "ctrl": True, "shift": True},
+ {"keep_audio": True},
+ "Import Local Footage",
+ )
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ keep_audio: bpy.props.BoolProperty(
+ name="Keep Audio from Video Files",
+ description=("If False, the audio that comes with video files will" " not be imported"),
+ default=True,
+ )
+ img_length: bpy.props.FloatProperty(
+ name="Image strip Length",
+ description="Controls the duration of the imported image strip in seconds",
+ default=3.0,
+ min=1.0,
+ )
+ img_padding: bpy.props.FloatProperty(
+ name="Image Padding",
+ description="Padding added between imported image strips in seconds",
+ default=1.0,
+ min=0.0,
+ )
+
+ sequencer_area = None
+ directory = ""
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.data.is_saved
+
+ def execute(self, context):
+ self.sequencer_area = self.get_sequencer_area(context)
+ self.directory = os.path.split(bpy.data.filepath)[0]
+
+ filepaths = self.find_local_footage_files()
+ files_to_import = [os.path.join(self.directory, f) for f in self.find_new_files_to_import(filepaths)]
+ print(files_to_import)
+ if not files_to_import:
+ self.report({"INFO"}, "No new files to import found")
+ return {"FINISHED"}
+
+ bpy.ops.screen.animation_cancel(restore_frame=True)
+
+ audio = self.import_audios(
+ context, [f for f in files_to_import if f.lower().endswith(EXTENSIONS_AUDIO)]
+ )
+ video = self.import_videos(
+ context, [f for f in files_to_import if f.lower().endswith(EXTENSIONS_VIDEO)]
+ )
+ img = self.import_imgs(
+ context, [f for f in files_to_import if f.lower().endswith(EXTENSIONS_IMG)]
+ )
+
+ bpy.data.texts["POWER_SEQUENCER_IMPORTS"].from_string(json.dumps(filepaths))
+
+ for s in audio:
+ s.show_waveform = True
+
+ imported = audio + video + img
+ for s in imported:
+ s.select = True
+ self.set_selected_strips_proxies(context)
+ self.report({"INFO"}, "Imported {!s} strips from newly found files.".format(len(imported)))
+ return {"FINISHED"}
+
+ def get_sequencer_area(self, context):
+ """
+ Returns the sequencer area to use as a context override in
+ some operators
+ """
+ sequencer_area = None
+ for window in context.window_manager.windows:
+ for area in window.screen.areas:
+ if not area.type == "SEQUENCE_EDITOR":
+ continue
+ sequencer_area = {
+ "window": window,
+ "screen": window.screen,
+ "area": area,
+ "scene": context.scene,
+ }
+ return sequencer_area
+
+ def find_local_footage_files(self, ignored_directories=["BL_proxy"]):
+ """
+ Returns a list of relative filepaths in all subdirectories of the `self.directory`
+ for all valid files that can be imported in the Sequencer
+ """
+ files_list = []
+ for root, dirs, files in os.walk(self.directory):
+ for directory in ignored_directories:
+ if directory in dirs:
+ dirs.remove(directory)
+
+ files = [f for f in sorted(files) if f.lower().endswith(EXTENSIONS_ALL)]
+ files = map(lambda name: os.path.join(root, name), files)
+ files = map(lambda path: os.path.relpath(path, root), files)
+ files_list.extend(list(files))
+
+ return files_list
+
+ def create_import_text_block(self, name):
+ """
+ Creates a new text data block that contains an empty json list, renames it to `name` and
+ returns it
+ """
+ re_text = re.compile(r"^Text.[0-9]{3}$")
+
+ bpy.ops.text.new()
+
+ # Find the newly created text file's identifier
+ ids = [text.name for text in bpy.data.texts if text.name.startswith("Text")]
+ id = max(ids)
+
+ text_file = bpy.data.texts[id]
+ text_file.name = name
+ text_file.from_string("[]")
+ return text_file
+
+ def find_new_files_to_import(self, filepaths):
+ """
+ Gets and optionally creates the list of already imported files in this project
+ Returns a list of paths from filepaths that are not in the imported text file
+ """
+ text_file = (
+ bpy.data.texts.get("POWER_SEQUENCER_IMPORTS")
+ if "POWER_SEQUENCER_IMPORTS" in bpy.data.texts.keys()
+ else self.create_import_text_block("POWER_SEQUENCER_IMPORTS")
+ )
+ imported_files = json.loads(text_file.as_string())
+ files_to_import = [p for p in filepaths if p not in imported_files]
+ return files_to_import
+
+ def import_videos(self, context, videos_filepaths):
+ """
+ Imports a list of files using movie_strip_add
+ Returns the list of imported sequences
+ """
+ frame = context.scene.frame_current
+
+ context.window_manager.progress_begin(0, len(videos_filepaths))
+ imported = []
+ for index, f in enumerate(videos_filepaths):
+ is_first_import = index == 0
+ bpy.ops.sequencer.movie_strip_add(
+ self.sequencer_area,
+ filepath=f,
+ frame_start=frame,
+ sound=self.keep_audio,
+ use_framerate=is_first_import,
+ )
+ imported.extend(context.selected_sequences)
+ frame = context.selected_sequences[0].frame_final_end
+ context.window_manager.progress_update(index)
+
+ context.window_manager.progress_end()
+ return imported
+
+ def import_audios(self, context, audio_filepaths):
+ """
+ Imports audio files as sound strips from a list of absolute file paths
+ Returns the list of newly imported audio files
+ """
+ frame = context.scene.frame_current
+ imported = []
+ for f in audio_filepaths:
+ bpy.ops.sequencer.sound_strip_add(self.sequencer_area, filepath=f, frame_start=frame)
+ imported.extend(context.selected_sequences)
+ frame = context.selected_sequences[0].frame_final_end
+ return imported
+
+ def import_imgs(self, context, img_filepaths):
+ frame = context.scene.frame_current
+ strip_length = convert_duration_to_frames(context, self.img_length)
+ strip_padding = convert_duration_to_frames(context, self.img_padding)
+
+ new_sequences = []
+ for f in img_filepaths:
+ head, tail = os.path.split(f)
+ bpy.ops.sequencer.image_strip_add(
+ self.sequencer_area,
+ directory=head,
+ files=[{"name": tail}],
+ frame_start=frame,
+ frame_end=frame + strip_length,
+ )
+ frame += strip_length + strip_padding
+ new_sequences.extend(context.selected_sequences)
+
+ return new_sequences
+
+ def set_selected_strips_proxies(self, context):
+ proxy_sizes = ["25", "50", "75", "100"]
+
+ use_proxy = False
+ prefs = get_preferences(context)
+ for size in proxy_sizes:
+ if hasattr(prefs, "proxy_" + size):
+ use_proxy = True
+ break
+
+ if not use_proxy:
+ return
+
+ for s in [s for s in context.selected_sequences if s.type in ["MOVIE", "IMAGE"]]:
+ s.use_proxy = True
+ s.proxy.build_25 = prefs.proxy_25
+ s.proxy.build_50 = prefs.proxy_50
+ s.proxy.build_75 = prefs.proxy_75
+ s.proxy.build_100 = prefs.proxy_100
diff --git a/power_sequencer/operators/jump_time_offset.py b/power_sequencer/operators/jump_time_offset.py
new file mode 100644
index 00000000..bc72e98e
--- /dev/null
+++ b/power_sequencer/operators/jump_time_offset.py
@@ -0,0 +1,82 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import convert_duration_to_frames
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_jump_time_offset(bpy.types.Operator):
+ """
+ *brief* Jump forward or backward in time
+
+
+ Move the time cursor forward or backward, using a duration in seconds.
+
+ The equivalent tool in Blender only works with frames, meaning the jump
+ will be different if your project's framerate is different. This tool
+ fixes that issue.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "RIGHT_ARROW", "value": "PRESS", "shift": True},
+ {"direction": "forward"},
+ "Jump Forward",
+ ),
+ (
+ {"type": "LEFT_ARROW", "value": "PRESS", "shift": True},
+ {"direction": "backward"},
+ "Jump Backward",
+ ),
+ ],
+ "keymap": "Frames",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER"}
+
+ duration: bpy.props.FloatProperty(
+ name="Duration",
+ description="The length of the jump in seconds (default: 1.0)",
+ default=1.0,
+ min=0,
+ )
+ direction: bpy.props.EnumProperty(
+ name="Direction",
+ description="Jump direction, either forward or backward",
+ items=[
+ ("forward", "Forward", "Jump forward in time"),
+ ("backward", "Backward", "Jump backward in time"),
+ ],
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene
+
+ def execute(self, context):
+ direction = 1 if self.direction == "forward" else -1
+ context.scene.frame_current += (
+ convert_duration_to_frames(context, self.duration) * direction
+ )
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/jump_to_cut.py b/power_sequencer/operators/jump_to_cut.py
new file mode 100644
index 00000000..8de9224a
--- /dev/null
+++ b/power_sequencer/operators/jump_to_cut.py
@@ -0,0 +1,119 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_jump_to_cut(bpy.types.Operator):
+ """
+ *brief* Jump to next/previous cut
+
+
+ Jump to the next or the previous cut in the edit. Unlike Blender's default tool, also
+ works during playback.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "UP_ARROW", "value": "PRESS"},
+ {"direction": "RIGHT"},
+ "Jump to next cut or keyframe",
+ ),
+ (
+ {"type": "DOWN_ARROW", "value": "PRESS"},
+ {"direction": "LEFT"},
+ "Jump to previous cut or keyframe",
+ ),
+ ],
+ "keymap": "Frames",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ direction: bpy.props.EnumProperty(
+ name="Direction",
+ description="Jump direction, either forward or backward",
+ items=[
+ ("RIGHT", "Forward", "Jump forward in time"),
+ ("LEFT", "Backward", "Jump backward in time"),
+ ],
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ frame_current = context.scene.frame_current
+ sorted_sequences = sorted(
+ context.sequences, key=attrgetter("frame_final_start", "frame_final_end")
+ )
+
+ fcurves = []
+ animation_data = context.scene.animation_data
+ if animation_data and animation_data.action:
+ fcurves = animation_data.action.fcurves
+
+ frame_target = -1
+ if self.direction == "RIGHT":
+ sequences = [s for s in sorted_sequences if s.frame_final_end > frame_current]
+ for s in sequences:
+
+ frame_target = (
+ s.frame_final_end
+ if s.frame_final_start <= frame_current
+ else s.frame_final_start
+ )
+
+ for f in fcurves:
+ for k in f.keyframe_points:
+ frame = k.co[0]
+ if frame <= context.scene.frame_current:
+ continue
+ frame_target = min(frame_target, frame)
+ break
+
+ elif self.direction == "LEFT":
+ sequences = [
+ s for s in reversed(sorted_sequences) if s.frame_final_start < frame_current
+ ]
+ for s in sequences:
+
+ frame_target = (
+ s.frame_final_start if s.frame_final_end >= frame_current else s.frame_final_end
+ )
+
+ for f in fcurves:
+ for k in f.keyframe_points:
+ frame = k.co[0]
+ if frame >= context.scene.frame_current:
+ continue
+ frame_target = max(frame_target, frame)
+ break
+
+ if frame_target != -1:
+ context.scene.frame_current = max(1, frame_target)
+
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/make_still_image.py b/power_sequencer/operators/make_still_image.py
new file mode 100644
index 00000000..73a3ed66
--- /dev/null
+++ b/power_sequencer/operators/make_still_image.py
@@ -0,0 +1,121 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import operator
+
+from .utils.global_settings import SequenceTypes
+from .utils.functions import convert_duration_to_frames
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_make_still_image(bpy.types.Operator):
+ """
+ *brief* Make still image from active strip
+
+
+ Converts image under the cursor to a still image, to create a pause effect in the video,
+ using the active sequence
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ strip_duration: bpy.props.FloatProperty(
+ name="Strip Duration",
+ description="The duration in seconds of the new strip, if 0.0 it will use the gap as its duration",
+ default=0.0,
+ min=0.0,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def invoke(self, context, event):
+ window_manager = context.window_manager
+ return window_manager.invoke_props_dialog(self)
+
+ def execute(self, context):
+ scene = context.scene
+ active = scene.sequence_editor.active_strip
+ sequencer = bpy.ops.sequencer
+ transform = bpy.ops.transform
+
+ start_frame = scene.frame_current
+ offset = convert_duration_to_frames(context, self.strip_duration)
+
+ if active.type not in SequenceTypes.VIDEO:
+ self.report(
+ {"ERROR_INVALID_INPUT"},
+ "You must select a video or meta strip. \
+ You selected a strip of type"
+ + str(active.type)
+ + " instead.",
+ )
+ return {"CANCELLED"}
+
+ if not active.frame_final_start <= start_frame < active.frame_final_end:
+ self.report(
+ {"ERROR_INVALID_INPUT"},
+ "Your time cursor must be on the frame you want \
+ to convert to a still image.",
+ )
+ return {"CANCELLED"}
+
+ if start_frame == active.frame_final_start:
+ scene.frame_current = start_frame + 1
+
+ if self.strip_duration <= 0.0:
+ strips = sorted(
+ scene.sequence_editor.sequences, key=operator.attrgetter("frame_final_start")
+ )
+
+ for s in strips:
+ if s.frame_final_start > active.frame_final_start and s.channel == active.channel:
+ next = s
+ break
+ offset = next.frame_final_start - active.frame_final_end
+
+ active.select = True
+ source_blend_type = active.blend_type
+ sequencer.cut(frame=scene.frame_current, type="SOFT", side="RIGHT")
+ transform.seq_slide(value=(offset, 0))
+ sequencer.cut(frame=scene.frame_current + offset + 1, type="SOFT", side="LEFT")
+ transform.seq_slide(value=(-offset, 0))
+
+ sequencer.meta_make()
+ active = scene.sequence_editor.active_strip
+ active.name = "Still image"
+ active.blend_type = source_blend_type
+ active.select_right_handle = True
+ transform.seq_slide(value=(offset, 0))
+
+ scene.frame_current = start_frame
+
+ active.select = True
+ active.select_right_handle = False
+ active.select_left_handle = False
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/marker_delete_closest.py b/power_sequencer/operators/marker_delete_closest.py
new file mode 100644
index 00000000..e5c8d2c0
--- /dev/null
+++ b/power_sequencer/operators/marker_delete_closest.py
@@ -0,0 +1,49 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_marker_delete_closest(bpy.types.Operator):
+ """
+ Deletes the marker closest to the time cursor
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Markers",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.timeline_markers
+
+ def invoke(self, context, event):
+ markers = context.scene.timeline_markers
+ frame = context.scene.frame_current
+
+ closest_marker = min(markers, key=lambda marker: abs(frame - marker.frame))
+ markers.remove(closest_marker)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/marker_delete_direct.py b/power_sequencer/operators/marker_delete_direct.py
new file mode 100644
index 00000000..153e494e
--- /dev/null
+++ b/power_sequencer/operators/marker_delete_direct.py
@@ -0,0 +1,50 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_marker_delete_direct(bpy.types.Operator):
+ """
+ Delete selected markers instantly skipping the default confirmation prompt
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "X", "value": "PRESS"}, {}, "Delete Markers Instantly")],
+ "keymap": "Markers",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.timeline_markers
+
+ def execute(self, context):
+ markers = context.scene.timeline_markers
+
+ selected_markers = [m for m in markers if m.select]
+ for m in selected_markers:
+ markers.remove(m)
+ self.report({"INFO"}, "Deleted %s markers." % len(selected_markers))
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/marker_go_to_next.py b/power_sequencer/operators/marker_go_to_next.py
new file mode 100644
index 00000000..014091e5
--- /dev/null
+++ b/power_sequencer/operators/marker_go_to_next.py
@@ -0,0 +1,74 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import find_neighboring_markers
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_marker_go_to_next(bpy.types.Operator):
+ """
+ Moves the time cursor to the next marker
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ target_marker: bpy.props.EnumProperty(
+ items=[("left", "left", "left"), ("right", "right", "right")],
+ name="Target marker",
+ description="Move to the closest marker to the left or to the right of the cursor",
+ default="left",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene
+
+ def execute(self, context):
+ if not context.scene.timeline_markers:
+ self.report({"ERROR_INVALID_INPUT"}, "There are no markers. Operation cancelled.")
+ return {"CANCELLED"}
+
+ frame = context.scene.frame_current
+ previous_marker, next_marker = find_neighboring_markers(context, frame)
+
+ if (
+ not previous_marker
+ and self.target_marker == "left"
+ or not next_marker
+ and self.target_marker == "right"
+ ):
+ self.report({"INFO"}, "No more markers to jump to on the %s side." % self.target_marker)
+ return {"CANCELLED"}
+
+ previous_time = previous_marker.frame if previous_marker else None
+ next_time = next_marker.frame if next_marker else None
+
+ context.scene.frame_current = (
+ previous_time if self.target_marker == "left" or not next_time else next_time
+ )
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/marker_snap_to_cursor.py b/power_sequencer/operators/marker_snap_to_cursor.py
new file mode 100644
index 00000000..aa480761
--- /dev/null
+++ b/power_sequencer/operators/marker_snap_to_cursor.py
@@ -0,0 +1,61 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_marker_snap_to_cursor(bpy.types.Operator):
+ """
+ Snap selected marker to the time cursor
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.timeline_markers
+
+ def execute(self, context):
+ markers = context.scene.timeline_markers
+
+ selected_markers = []
+ for marker in markers:
+ if marker.select:
+ selected_markers.append(marker)
+
+ if not selected_markers:
+ return {"CANCELLED"}
+ if len(selected_markers) > 1:
+ self.report(
+ {"ERROR_INVALID_INPUT"},
+ "You can only snap 1 marker at a time. Operation cancelled.",
+ )
+ return {"CANCELLED"}
+
+ selected_markers[0].frame = context.scene.frame_current
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/markers_as_timecodes.py b/power_sequencer/operators/markers_as_timecodes.py
new file mode 100644
index 00000000..642d6118
--- /dev/null
+++ b/power_sequencer/operators/markers_as_timecodes.py
@@ -0,0 +1,67 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import datetime as dt
+import operator as op
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_copy_markers_as_timecodes(bpy.types.Operator):
+ """
+ Formats and copies all the markers as timecodes to put in a Youtube video's description
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.timeline_markers
+
+ def execute(self, context):
+ render = context.scene.render
+
+ if len(context.scene.timeline_markers) == 0:
+ self.report({"INFO"}, "No markers found")
+ return {"CANCELLED"}
+
+ sorted_markers = sorted(context.scene.timeline_markers,
+ key=lambda m: m.frame)
+
+ framerate = render.fps / render.fps_base
+ last_marker_seconds = sorted_markers[-1].frame / framerate
+ seconds_in_hour = 3600.0
+ time_format = "%H:%M:%S" if last_marker_seconds >= seconds_in_hour else "%M:%S"
+
+ markers_as_timecodes = []
+ for marker in sorted_markers:
+ time = dt.datetime(year=1, month=1, day=1) + dt.timedelta(
+ seconds=marker.frame / framerate)
+ markers_as_timecodes.append(
+ time.strftime(time_format) + " " + marker.name)
+
+ bpy.context.window_manager.clipboard = "\n".join(markers_as_timecodes)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/markers_create_from_selected.py b/power_sequencer/operators/markers_create_from_selected.py
new file mode 100644
index 00000000..94ebafc9
--- /dev/null
+++ b/power_sequencer/operators/markers_create_from_selected.py
@@ -0,0 +1,59 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_markers_create_from_selected_strips(bpy.types.Operator):
+ """
+ *brief* Create one marker at the start on each selected strip, based on its name
+
+ Use it to copy markers as timecodes.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ cursor_frame_start = context.scene.frame_current
+
+ for m in context.scene.timeline_markers:
+ m.select = False
+
+ for s in context.selected_sequences:
+ bpy.ops.marker.add()
+ new_marker = context.scene.timeline_markers[-1]
+
+ new_marker.select = True
+ bpy.ops.marker.rename(name=s.name)
+ gap = s.frame_final_start - cursor_frame_start
+ bpy.ops.marker.move(frames=gap)
+ new_marker.select = False
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/markers_set_preview_in_between.py b/power_sequencer/operators/markers_set_preview_in_between.py
new file mode 100644
index 00000000..f505c278
--- /dev/null
+++ b/power_sequencer/operators/markers_set_preview_in_between.py
@@ -0,0 +1,69 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import find_neighboring_markers
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_set_preview_between_markers(bpy.types.Operator):
+ """
+ Set the timeline's preview range using the 2 markers closest to the time cursor
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor
+
+ def invoke(self, context, event):
+ if not context.scene.timeline_markers:
+ self.report({"ERROR_INVALID_INPUT"}, "There are no markers. Operation cancelled.")
+ return {"CANCELLED"}
+
+ frame = context.scene.frame_current
+ previous_marker, next_marker = find_neighboring_markers(context, frame)
+
+ if not (previous_marker and next_marker):
+ self.report({"ERROR_INVALID_INPUT"}, "There are no markers. Operation cancelled.")
+ return {"CANCELLED"}
+
+ frame_start = previous_marker.frame if previous_marker else 0
+ if next_marker:
+ frame_end = next_marker.frame
+ else:
+ from operator import attrgetter
+
+ frame_end = max(
+ context.scene.sequence_editor.sequences, key=attrgetter("frame_final_end")
+ ).frame_final_end
+
+ from .utils.functions import set_preview_range
+
+ set_preview_range(context, frame_start, frame_end)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/markers_snap_matching_strips.py b/power_sequencer/operators/markers_snap_matching_strips.py
new file mode 100644
index 00000000..bdfba314
--- /dev/null
+++ b/power_sequencer/operators/markers_snap_matching_strips.py
@@ -0,0 +1,50 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_markers_snap_matching_strips(bpy.types.Operator):
+ """
+ Snap selected strips to markers with the same name
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.timeline_markers
+
+ def execute(self, context):
+ timeline_markers = context.scene.timeline_markers
+
+ for strip in context.selected_sequences:
+ for marker in timeline_markers:
+ if marker.name in strip.name:
+ strip.frame_start = marker.frame - strip.frame_offset_start
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/meta_resize_to_content.py b/power_sequencer/operators/meta_resize_to_content.py
new file mode 100644
index 00000000..0697104e
--- /dev/null
+++ b/power_sequencer/operators/meta_resize_to_content.py
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from .utils.functions import get_frame_range
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_meta_resize_to_content(bpy.types.Operator):
+ """
+ *brief* Moves the handles of the selected metastrip so it fits its content
+
+
+ Use it to trim a metastrip quickly
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ selected_meta_strips = (s for s in context.selected_sequences if s.type == "META")
+ for s in selected_meta_strips:
+ s.frame_final_start, s.frame_final_end = get_frame_range(context, s.sequences)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/meta_trim_content_to_bounds.py b/power_sequencer/operators/meta_trim_content_to_bounds.py
new file mode 100644
index 00000000..0e77681b
--- /dev/null
+++ b/power_sequencer/operators/meta_trim_content_to_bounds.py
@@ -0,0 +1,63 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from .utils.global_settings import SequenceTypes
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_meta_trim_content_to_bounds(bpy.types.Operator):
+ """
+ Deletes and trims the strips inside selected meta-strips to the meta strip's bounds
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ to_delete = []
+ meta_strips = [s for s in context.selected_sequences if s.type == "META"]
+ for m in meta_strips:
+ start, end = m.frame_final_start, m.frame_final_end
+ sequences_to_process = (s for s in m.sequences if s.type not in SequenceTypes.EFFECT)
+ for s in sequences_to_process:
+ if s.frame_final_end < start or s.frame_final_start > m.frame_final_end:
+ to_delete.append(s)
+ continue
+ # trim strips on the meta's edges or longer than the meta's extents
+ if s.frame_final_start < start:
+ s.frame_final_start = start
+ if s.frame_final_end > end:
+ s.frame_final_end = end
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in to_delete:
+ s.select = True
+ bpy.ops.sequencer.delete()
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/meta_ungroup_and_trim.py b/power_sequencer/operators/meta_ungroup_and_trim.py
new file mode 100644
index 00000000..e639570a
--- /dev/null
+++ b/power_sequencer/operators/meta_ungroup_and_trim.py
@@ -0,0 +1,60 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_meta_ungroup_and_trim(bpy.types.Operator):
+ """
+ UnMeta all selected meta strips and trim their content
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ trim_content: bpy.props.BoolProperty(
+ name="Trim Content",
+ description="Trim the content of the Meta Strips to their extents",
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ meta_strips = [s for s in context.selected_sequences if s.type == "META"]
+ if self.trim_content:
+ bpy.ops.power_sequencer.meta_trim_content_to_bounds()
+ self.separate(context, meta_strips)
+ return {"FINISHED"}
+
+ def separate(self, context, meta_strips):
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for m in meta_strips:
+ context.scene.sequence_editor.active_strip = m
+ bpy.ops.sequencer.meta_separate()
diff --git a/power_sequencer/operators/mouse_toggle_mute.py b/power_sequencer/operators/mouse_toggle_mute.py
new file mode 100644
index 00000000..44a1e7d5
--- /dev/null
+++ b/power_sequencer/operators/mouse_toggle_mute.py
@@ -0,0 +1,64 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""Toggle mute a sequence as you click on it"""
+import bpy
+from math import floor
+
+from .utils.functions import find_strips_mouse
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_mouse_toggle_mute(bpy.types.Operator):
+ """
+ Toggle mute a sequence as you click on it
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "LEFTMOUSE", "value": "PRESS", "alt": True}, {}, "Mouse Toggle Mute")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ sequencer = bpy.ops.sequencer
+
+ # get current frame and channel the mouse hovers
+ x, y = context.region.view2d.region_to_view(x=event.mouse_region_x, y=event.mouse_region_y)
+ frame, channel = round(x), floor(y)
+
+ # Strip selection
+ sequencer.select_all(action="DESELECT")
+ to_select = find_strips_mouse(context, frame, channel)
+
+ if not to_select:
+ return {"CANCELLED"}
+
+ for s in to_select:
+ s.mute = not s.mute
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/mouse_trim_instantly.py b/power_sequencer/operators/mouse_trim_instantly.py
new file mode 100644
index 00000000..dd2bb1fd
--- /dev/null
+++ b/power_sequencer/operators/mouse_trim_instantly.py
@@ -0,0 +1,113 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from math import floor
+
+from .utils.functions import find_strips_mouse
+from .utils.functions import trim_strips
+from .utils.functions import get_frame_range
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+from .utils.functions import sequencer_workaround_2_80_audio_bug
+
+
+class POWER_SEQUENCER_OT_mouse_trim_instantly(bpy.types.Operator):
+ """
+ *brief* Trim strip from a start to an end frame instantly
+
+
+ Trims a frame range or a selection from a start to an end frame.
+ If there's no precise time range, auto trims based on the closest cut
+
+ Args:
+ - frame_start and frame_end (int) define the frame range to trim
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "RIGHTMOUSE", "value": "PRESS", "ctrl": True, "alt": True},
+ {"select_mode": "CONTEXT"},
+ "Trim strip, keep gap",
+ ),
+ (
+ {"type": "RIGHTMOUSE", "value": "PRESS", "ctrl": True, "alt": True, "shift": True},
+ {"select_mode": "CURSOR"},
+ "Trim strip, remove gap",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ select_mode: bpy.props.EnumProperty(
+ items=[
+ ("CONTEXT", "Smart", "Uses the selection if possible, else uses the other modes"),
+ ("CURSOR", "Time cursor", "Select all of the strips the time cursor overlaps"),
+ ],
+ name="Selection mode",
+ description="Auto-select the strip you click on or that the time cursor overlaps",
+ default="CONTEXT",
+ )
+ select_linked: bpy.props.BoolProperty(
+ name="Use linked time",
+ description="If auto-select, cut linked strips if checked",
+ default=False,
+ )
+ gap_remove: bpy.props.BoolProperty(
+ name="Remove gaps",
+ description="When trimming the sequences, remove gaps automatically",
+ default=True,
+ )
+
+ to_select = []
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ to_select = []
+ frame, channel = -1, -1
+ x, y = context.region.view2d.region_to_view(
+ x=event.mouse_region_x, y=event.mouse_region_y
+ )
+ frame, channel = round(x), floor(y)
+
+ mouse_clicked_strip = find_strips_mouse(context, frame, channel, self.select_linked)
+ to_select.extend(mouse_clicked_strip)
+ if self.select_mode == "CURSOR":
+ to_select.extend([s for s in context.sequences if s.frame_final_start <= frame <= s.frame_final_end and not s.lock])
+
+ frame_cut_closest = min(get_frame_range(context, to_select), key=lambda f: abs(frame - f))
+ frame_start = min(frame, frame_cut_closest)
+ frame_end = max(frame, frame_cut_closest)
+
+ trim_strips(context, frame_start, frame_end, self.select_mode, to_select)
+
+ context.scene.frame_current = frame_start
+ if self.gap_remove and self.select_mode == "CURSOR":
+ bpy.ops.power_sequencer.gap_remove()
+
+ # FIXME: Workaround Blender 2.80's audio bug, remove when fixed in Blender
+ sequencer_workaround_2_80_audio_bug(context)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/mouse_trim_modal.py b/power_sequencer/operators/mouse_trim_modal.py
new file mode 100644
index 00000000..96da5290
--- /dev/null
+++ b/power_sequencer/operators/mouse_trim_modal.py
@@ -0,0 +1,408 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import bgl
+import gpu
+from gpu_extras.batch import batch_for_shader
+import math
+from mathutils import Vector
+
+from .utils.functions import (
+ find_strips_mouse,
+ trim_strips,
+ find_snap_candidate,
+ find_closest_surrounding_cuts,
+ sequencer_workaround_2_80_audio_bug,
+)
+
+from .utils.draw import (
+ draw_line,
+ draw_rectangle,
+ draw_triangle_equilateral,
+ draw_arrow_head,
+ get_color_gizmo_primary,
+ get_color_gizmo_secondary,
+)
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+if not bpy.app.background:
+ SHADER = gpu.shader.from_builtin("2D_UNIFORM_COLOR")
+
+
+class POWER_SEQUENCER_OT_mouse_trim(bpy.types.Operator):
+ """
+ *brief* Cut or Trim strips quickly with the mouse cursor
+
+
+ Click somehwere in the Sequencer to insert a cut, click and drag to trim
+ With this function you can quickly cut and remove a section of strips while keeping or
+ collapsing the remaining gap.
+ Press <kbd>Ctrl</kbd> to snap to cuts.
+
+ A [video demo](https://youtu.be/GiLmDhmMVAM?t=1m35s) is available.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/wVvX4ex.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "T", "value": "PRESS"},
+ {"select_mode": "CONTEXT", "gap_remove": False},
+ "Trim using the mouse cursor",
+ ),
+ (
+ {"type": "T", "value": "PRESS", "shift": True},
+ {"select_mode": "CURSOR", "gap_remove": True},
+ "Trim in all channels",
+ ),
+ (
+ {"type": "T", "value": "PRESS", "shift": True, "alt": True},
+ {"select_mode": "CURSOR", "gap_remove": True},
+ "Trim in all channels and remove gaps",
+ ),
+ (
+ {"type": "T", "value": "PRESS", "ctrl": True},
+ {"select_mode": "CONTEXT", "gap_remove": False},
+ "Trim using the mouse cursor",
+ ),
+ (
+ {"type": "T", "value": "PRESS", "ctrl": True, "alt": True},
+ {"select_mode": "CONTEXT", "gap_remove": True},
+ "Trim using the mouse cursor and remove gaps",
+ ),
+ (
+ {"type": "T", "value": "PRESS", "ctrl": True, "shift": True},
+ {"select_mode": "CURSOR", "gap_remove": True},
+ "Trim in all channels",
+ ),
+ (
+ {"type": "T", "value": "PRESS", "ctrl": True, "shift": True, "alt": True},
+ {"select_mode": "CURSOR", "gap_remove": True},
+ "Trim in all channels and remove gaps",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ select_mode: bpy.props.EnumProperty(
+ items=[
+ ("CURSOR", "Time cursor", "Select all of the strips the time cursor overlaps"),
+ ("CONTEXT", "Smart", "Uses the selection if possible, else uses the other modes"),
+ ],
+ name="Selection mode",
+ description="Cut only the strip under the mouse or all strips under the time cursor",
+ default="CONTEXT",
+ )
+ select_linked: bpy.props.BoolProperty(
+ name="Use linked time",
+ description="In mouse or CONTEXT mode, always cut linked strips if this is checked",
+ default=False,
+ )
+ gap_remove: bpy.props.BoolProperty(
+ name="Remove gaps",
+ description="When trimming the sequences, remove gaps automatically",
+ default=True,
+ )
+
+ TABLET_TRIM_DISTANCE_THRESHOLD = 6
+ # Don't rename these variables, we're using setattr to access them dynamically
+ trim_start, channel_start = 0, 0
+ trim_end, channel_end = 0, 0
+ is_trimming = False
+ trim_side = "end"
+
+ mouse_start_y = -1.0
+
+ draw_handler = None
+
+ use_audio_scrub = False
+
+ event_shift_released = True
+ event_alt_released = True
+
+ event_ripple, event_ripple_string = "LEFT_ALT", "Alt"
+ event_select_mode, event_select_mode_string = "LEFT_SHIFT", "Shift"
+ event_change_side = "O"
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ if context.screen.is_animation_playing:
+ bpy.ops.screen.animation_cancel(restore_frame=False)
+
+ self.use_audio_scrub = context.scene.use_audio_scrub
+ context.scene.use_audio_scrub = False
+
+ self.mouse_start_y = event.mouse_region_y
+
+ self.trim_initialize(context, event)
+ self.update_frame(context, event)
+ self.draw_start(context, event)
+ self.update_header_text(context, event)
+
+ context.window_manager.modal_handler_add(self)
+ return {"RUNNING_MODAL"}
+
+ def modal(self, context, event):
+
+ if event.type == self.event_change_side and event.value == "PRESS":
+ self.trim_side = "start" if self.trim_side == "end" else "end"
+
+ if event.type == self.event_ripple and event.value == "PRESS":
+ self.gap_remove = False if self.gap_remove else True
+
+ if event.type == self.event_select_mode and event.value == "PRESS":
+ self.select_mode = "CONTEXT" if self.select_mode == "CURSOR" else "CURSOR"
+
+ if event.type in {"ESC"}:
+ self.draw_stop()
+ context.scene.use_audio_scrub = self.use_audio_scrub
+ return {"FINISHED"}
+
+ # Start and end trim
+ if event.type == "LEFTMOUSE" or (event.type in ["RET", "T"] and event.value == "PRESS"):
+ self.trim_apply(context, event)
+ self.draw_stop()
+ context.scene.use_audio_scrub = self.use_audio_scrub
+
+ # FIXME: Workaround Blender 2.80's audio bug, remove when fixed in Blender
+ sequencer_workaround_2_80_audio_bug(context)
+
+ return {"FINISHED"}
+
+ # Update trim
+ if event.type == "MOUSEMOVE":
+ self.draw_stop()
+ self.update_frame(context, event)
+ self.draw_start(context, event)
+ self.update_header_text(context, event)
+ return {"PASS_THROUGH"}
+
+ return {"RUNNING_MODAL"}
+
+ def trim_initialize(self, context, event):
+ frame, self.channel_start = get_frame_and_channel(event)
+ self.trim_start = find_snap_candidate(context, frame) if event.ctrl else frame
+ self.trim_end, self.channel_end = self.trim_start, self.channel_start
+ self.is_trimming = True
+
+ def update_frame(self, context, event):
+ frame, channel = get_frame_and_channel(event)
+ frame_trim = find_snap_candidate(context, frame) if event.ctrl else frame
+ setattr(self, "channel_" + self.trim_side, channel)
+ setattr(self, "trim_" + self.trim_side, frame_trim)
+ context.scene.frame_current = getattr(self, "trim_" + self.trim_side)
+
+ def draw_start(self, context, event):
+ """Initializes the drawing handler, see draw()"""
+ to_select, to_delete = self.find_strips_to_trim(context)
+ target_strips = to_select + to_delete
+
+ draw_args = (self, context, self.trim_start, self.trim_end, target_strips, self.gap_remove)
+ self.draw_handler = bpy.types.SpaceSequenceEditor.draw_handler_add(
+ draw, draw_args, "WINDOW", "POST_PIXEL"
+ )
+
+ def draw_stop(self):
+ if self.draw_handler:
+ bpy.types.SpaceSequenceEditor.draw_handler_remove(self.draw_handler, "WINDOW")
+
+ def update_header_text(self, context, event):
+ text = (
+ "Trim from {} to {}".format(self.trim_start, self.trim_end)
+ + ", "
+ + "({}) Gap Remove {}".format(
+ self.event_ripple_string, "ON" if self.gap_remove else "OFF"
+ )
+ + ", "
+ + "({}) Mode: {}".format(self.event_select_mode_string, self.select_mode.capitalize())
+ + ", "
+ + "(Ctrl) Snap: {}".format("ON" if event.ctrl else "OFF")
+ + ", "
+ + "({}) Change Side".format(self.event_change_side)
+ )
+ context.area.header_text_set(text)
+
+ def trim_apply(self, context, event):
+ start_x = context.region.view2d.region_to_view(
+ x=event.mouse_region_x, y=event.mouse_region_y
+ )[0]
+ distance_to_start = abs(event.mouse_region_x - start_x)
+
+ is_cutting = (
+ self.trim_start == self.trim_end
+ or event.is_tablet
+ and distance_to_start <= self.TABLET_TRIM_DISTANCE_THRESHOLD
+ )
+ if is_cutting:
+ self.cut(context)
+ else:
+ self.trim(context)
+ self.is_trimming = False
+
+ def cut(self, context):
+ to_select = self.find_strips_to_cut(context)
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in to_select:
+ s.select = True
+
+ if len(to_select) == 0:
+ bpy.ops.power_sequencer.gap_remove()
+ else:
+ frame_current = context.scene.frame_current
+ context.scene.frame_current = self.trim_start
+ bpy.ops.sequencer.cut(frame=context.scene.frame_current, type="SOFT", side="BOTH")
+ context.scene.frame_current = frame_current
+
+ def find_strips_to_cut(self, context):
+ """
+ Returns a list of strips to cut, either the strip hovered by the mouse or all strips under
+ the time cursor, depending on the select_mode
+ """
+ to_cut, overlapping_strips = [], []
+ if self.select_mode == "CONTEXT":
+ overlapping_strips = find_strips_mouse(
+ context, self.trim_start, self.channel_start, self.select_linked
+ )
+ to_cut.extend(overlapping_strips)
+ if self.select_mode == "CURSOR" or (
+ not overlapping_strips and self.select_mode == "CONTEXT"
+ ):
+ to_cut = [
+ s
+ for s in context.sequences
+ if not s.lock and s.frame_final_start <= self.trim_start <= s.frame_final_end
+ ]
+ return to_cut
+
+ def trim(self, context):
+ to_select, to_delete = self.find_strips_to_trim(context)
+ trim_strips(context, self.trim_start, self.trim_end, self.select_mode, to_select, to_delete)
+ if (self.gap_remove and self.select_mode == "CURSOR") or (
+ self.select_mode == "CONTEXT" and to_select == [] and to_delete == []
+ ):
+ context.scene.frame_current = min(self.trim_start, self.trim_end)
+ bpy.ops.power_sequencer.gap_remove()
+ else:
+ context.scene.frame_current = self.trim_end
+
+ def find_strips_to_trim(self, context):
+ """
+ Returns two lists of strips to trim and strips to delete
+ """
+ to_trim, to_delete = [], []
+
+ trim_start = min(self.trim_start, self.trim_end)
+ trim_end = max(self.trim_start, self.trim_end)
+
+ channel_min = min(self.channel_start, self.channel_end)
+ channel_max = max(self.channel_start, self.channel_end)
+ channels = set(range(channel_min, channel_max + 1))
+
+ for s in context.sequences:
+ if s.lock:
+ continue
+ if self.select_mode == "CONTEXT" and s.channel not in channels:
+ continue
+
+ if trim_start <= s.frame_final_start and trim_end >= s.frame_final_end:
+ to_delete.append(s)
+ continue
+ if (
+ s.frame_final_start <= trim_start <= s.frame_final_end
+ or s.frame_final_start <= trim_end <= s.frame_final_end
+ ):
+ to_trim.append(s)
+
+ return to_trim, to_delete
+
+
+def draw(self, context, frame_start=-1, frame_end=-1, target_strips=[], draw_arrows=False):
+ """
+ Draws the line and arrows that represent the trim
+
+ Params:
+ - start_x and end_x are Vector(), the start_x and end_x of the drawn trim line's vertices in region coordinates
+ """
+ view_to_region = context.region.view2d.view_to_region
+
+ # Detect and draw the gap's limits if not trimming any strips
+ if not target_strips:
+ strip_before, strip_after = find_closest_surrounding_cuts(context, frame_end)
+ frame_start = strip_before.frame_final_end
+ frame_end = strip_after.frame_final_start
+ channels = [strip_before.channel, strip_after.channel]
+ else:
+ channels = {s.channel for s in target_strips}
+
+ start_x, start_y = view_to_region(
+ min(frame_start, frame_end), math.floor(min(channels)), clip=False
+ )
+ end_x, end_y = view_to_region(
+ max(frame_start, frame_end), math.floor(max(channels) + 1), clip=False
+ )
+
+ start_x = max(start_x, context.region.x)
+ start_y = max(start_y, context.region.y)
+
+ end_x = min(end_x, context.region.x + context.region.width)
+ end_y = min(end_y, context.region.y + context.region.height)
+
+ # Draw
+ color_line = get_color_gizmo_primary(context)
+ color_fill = color_line.copy()
+ color_fill[-1] = 0.3
+
+ rect_origin = Vector((start_x, start_y))
+ rect_size = Vector((end_x - start_x, abs(start_y - end_y)))
+
+ bgl.glEnable(bgl.GL_BLEND)
+ bgl.glLineWidth(3)
+ draw_rectangle(SHADER, rect_origin, rect_size, color_fill)
+ # Vertical lines
+ draw_line(SHADER, Vector((start_x, start_y)), Vector((start_x, end_y)), color_line)
+ draw_line(SHADER, Vector((end_x, start_y)), Vector((end_x, end_y)), color_line)
+
+ offset = 20.0
+ radius = 12.0
+ if draw_arrows and end_x - start_x > 2 * offset + radius:
+ center_y = (end_y + start_y) / 2.0
+ center_1 = Vector((start_x + offset, center_y))
+ center_2 = Vector((end_x - offset, center_y))
+ draw_triangle_equilateral(SHADER, center_1, radius, color=color_line)
+ draw_triangle_equilateral(SHADER, center_2, radius, math.pi, color=color_line)
+
+ bgl.glLineWidth(1)
+ bgl.glDisable(bgl.GL_BLEND)
+
+
+def get_frame_and_channel(event):
+ """
+ Returns a tuple of (frame, channel)
+ """
+ frame_float, channel_float = bpy.context.region.view2d.region_to_view(
+ x=event.mouse_region_x, y=event.mouse_region_y
+ )
+ return round(frame_float), math.floor(channel_float)
diff --git a/power_sequencer/operators/open_project_directory.py b/power_sequencer/operators/open_project_directory.py
new file mode 100644
index 00000000..4fc4cf38
--- /dev/null
+++ b/power_sequencer/operators/open_project_directory.py
@@ -0,0 +1,59 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import os
+from platform import system
+from subprocess import Popen
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_open_project_directory(bpy.types.Operator):
+ """
+ Opens the Blender project directory in file explorer
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.data.is_saved
+
+ def execute(self, context):
+ path = os.path.split(bpy.data.filepath)[0]
+
+ if not path:
+ self.report({"INFO"}, "You have to save your project first.")
+ return {"CANCELLED"}
+
+ if system() == "Windows":
+ Popen(["explorer", path])
+ elif system() == "Darwin":
+ Popen(["open", path])
+ else:
+ Popen(["xdg-open", path])
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/playback_speed_decrease.py b/power_sequencer/operators/playback_speed_decrease.py
new file mode 100644
index 00000000..5d5a9605
--- /dev/null
+++ b/power_sequencer/operators/playback_speed_decrease.py
@@ -0,0 +1,63 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_playback_speed_decrease(bpy.types.Operator):
+ """
+ *brief* Decrease playback speed incrementally down to normal
+
+
+ Playback speed may be set to any of the following speeds:
+
+ * Normal (1x)
+ * Fast (1.33x)
+ * Faster (1.66x)
+ * Double (2x)
+ * Triple (3x)
+
+ Activating this operator will decrease playback speed through each
+ of these steps until minimum speed is reached.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "COMMA", "value": "PRESS"}, {}, "Decrease Playback Speed")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ scene = context.scene
+
+ speeds = ["NORMAL", "FAST", "FASTER", "DOUBLE", "TRIPLE"]
+ playback_speed = scene.power_sequencer.playback_speed
+
+ index = max(0, speeds.index(playback_speed) - 1)
+ scene.power_sequencer.playback_speed = speeds[index]
+
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/playback_speed_increase.py b/power_sequencer/operators/playback_speed_increase.py
new file mode 100644
index 00000000..572bd9bc
--- /dev/null
+++ b/power_sequencer/operators/playback_speed_increase.py
@@ -0,0 +1,63 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_playback_speed_increase(bpy.types.Operator):
+ """
+ *brief* Increase playback speed up to triple
+
+
+ Playback speed may be set to any of the following speeds:
+
+ * Normal (1x)
+ * Fast (1.33x)
+ * Faster (1.66x)
+ * Double (2x)
+ * Triple (3x)
+
+ Activating this operator will increase playback speed through each
+ of these steps until maximum speed is reached.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "PERIOD", "value": "PRESS"}, {}, "Increase playback speed")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ scene = context.scene
+
+ speeds = ["NORMAL", "FAST", "FASTER", "DOUBLE", "TRIPLE"]
+ playback_speed = scene.power_sequencer.playback_speed
+
+ index = min(speeds.index(playback_speed) + 1, len(speeds) - 1)
+ scene.power_sequencer.playback_speed = speeds[index]
+
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/playback_speed_set.py b/power_sequencer/operators/playback_speed_set.py
new file mode 100644
index 00000000..2e21f7b9
--- /dev/null
+++ b/power_sequencer/operators/playback_speed_set.py
@@ -0,0 +1,68 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_playback_speed_set(bpy.types.Operator):
+ """
+ Change the playback_speed property using an operator property. Used with keymaps
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "NUMPAD_1", "ctrl": True, "value": "PRESS"}, {"speed": "NORMAL"}, "Speed to 1x"),
+ ({"type": "NUMPAD_2", "ctrl": True, "value": "PRESS"}, {"speed": "FAST"}, "Speed to 1.33x"),
+ (
+ {"type": "NUMPAD_3", "ctrl": True, "value": "PRESS"},
+ {"speed": "FASTER"},
+ "Speed to 1.66x",
+ ),
+ ({"type": "NUMPAD_4", "ctrl": True, "value": "PRESS"}, {"speed": "DOUBLE"}, "Speed to 2x"),
+ ({"type": "NUMPAD_5", "ctrl": True, "value": "PRESS"}, {"speed": "TRIPLE"}, "Speed to 3x"),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER"}
+
+ speed: bpy.props.EnumProperty(
+ items=[
+ ("NORMAL", "Normal (1x)", ""),
+ ("FAST", "Fast (1.33x)", ""),
+ ("FASTER", "Faster (1.66x)", ""),
+ ("DOUBLE", "Double (2x)", ""),
+ ("TRIPLE", "Triple (3x)", ""),
+ ],
+ name="Speed",
+ description="Change the playback speed",
+ default="DOUBLE",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ context.scene.power_sequencer.playback_speed = self.speed
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/preview_closest_cut.py b/power_sequencer/operators/preview_closest_cut.py
new file mode 100644
index 00000000..dccf69e3
--- /dev/null
+++ b/power_sequencer/operators/preview_closest_cut.py
@@ -0,0 +1,94 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_frame_range
+from .utils.functions import set_preview_range
+from .utils.functions import convert_duration_to_frames
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_preview_closest_cut(bpy.types.Operator):
+ """
+ *brief* Toggle preview around the closest cut, based on time cursor
+
+
+ Finds the closest cut to the time cursor and sets the preview to a small range around that
+ frame. If the preview matches the range, resets to the full timeline
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "P", "value": "PRESS", "shift": True}, {}, "Preview Last Cut")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ duration: bpy.props.FloatProperty(
+ name="Preview duration",
+ description="Total duration of the preview, in seconds",
+ default=1.0,
+ min=0.1,
+ )
+ cut_frame_override: bpy.props.IntProperty(
+ name="Cut Frame Override",
+ description="Force to preview around this frame",
+ default=0,
+ min=0,
+ options={"HIDDEN"},
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ scene = context.scene
+
+ preview_center = (
+ self.find_closest_cut_frame(context)
+ if not self.cut_frame_override
+ else self.cut_frame_override
+ )
+
+ duration_frame = convert_duration_to_frames(context, self.duration)
+ start = preview_center - duration_frame / 2
+ end = preview_center + duration_frame / 2
+ if not (preview_center > 1 and start > 1):
+ return {"CANCELLED"}
+
+ if scene.frame_preview_start == start and scene.frame_preview_end == end:
+ start, end = get_frame_range(context, context.sequences)
+ set_preview_range(context, start, end)
+ return {"FINISHED"}
+
+ def find_closest_cut_frame(self, context):
+ last_distance = 100000
+ closest_cut_frame = 0
+ for s in context.sequences:
+ cuts = [s.frame_final_start, s.frame_final_end]
+ for cut_frame in cuts:
+ distance_to_cut = abs(cut_frame - context.scene.frame_current)
+ if distance_to_cut < last_distance:
+ last_distance = distance_to_cut
+ closest_cut_frame = cut_frame
+ return closest_cut_frame
diff --git a/power_sequencer/operators/preview_to_selection.py b/power_sequencer/operators/preview_to_selection.py
new file mode 100644
index 00000000..20d168dd
--- /dev/null
+++ b/power_sequencer/operators/preview_to_selection.py
@@ -0,0 +1,56 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_frame_range
+from .utils.functions import set_preview_range
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_preview_to_selection(bpy.types.Operator):
+ """
+ *brief* Sets the timeline preview range to match the selection
+
+ Sets the scene frame start to the earliest frame start of selected sequences and the scene
+ frame end to the last frame of selected sequences.
+ Uses all sequences in the current context if no sequences are selected.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/EV1sUrn.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "P", "value": "PRESS", "ctrl": True, "alt": True}, {}, "Preview To Selection")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ scene = context.scene
+ sequences = context.selected_sequences if len(context.selected_sequences) >= 1 else context.sequences
+ frame_start, frame_end = get_frame_range(context, sequences)
+ set_preview_range(context, frame_start, frame_end - 1)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/render_apply_preset.py b/power_sequencer/operators/render_apply_preset.py
new file mode 100644
index 00000000..dee19dbd
--- /dev/null
+++ b/power_sequencer/operators/render_apply_preset.py
@@ -0,0 +1,128 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import os
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_render_apply_preset(bpy.types.Operator):
+ """
+ *Brief* Applies a rendering preset to the project
+
+ Sets rendering and encoding settings and an output filename based on a preset.
+
+ Available presets:
+
+ - YouTube: 1080p mp4 video encoded with H264 and AAC for audio, based on YouTube's recommended settings
+ - Twitter: 720p mp4 video
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "F12", "value": "PRESS", "alt": True},
+ {"preset": "youtube"},
+ "Apply Youtube Render Preset",
+ )
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER"}
+
+ preset: bpy.props.EnumProperty(
+ items=[
+ (
+ "youtube",
+ "youtube",
+ "Full HD mp4 with AAC audio, following recommendations from Youtube",
+ ),
+ (
+ "twitter",
+ "twitter",
+ "HD ready mp4 with high enough bitrate for Twitter and Facebook",
+ ),
+ ],
+ name="Preset",
+ description="Preset to use ",
+ default="youtube",
+ )
+
+ name_pattern: bpy.props.EnumProperty(
+ items=[
+ ("folder", "Folder", "Use the folder's name as the exported file name"),
+ (
+ "blender",
+ "Blender file",
+ "Use the project's .blend file name as the exported file name",
+ ),
+ ("scene", "Current scene", "Use the scene's name as the exported file name"),
+ ],
+ name="Filename",
+ description="Auto name the rendered video after...",
+ default="blender",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene
+
+ def execute(self, context):
+ if not bpy.data.is_saved:
+ self.report({"WARNING"}, "Save your file first")
+ return {"CANCELLED"}
+
+ script_file = os.path.realpath(__file__)
+ addon_directory = os.path.dirname(script_file)
+
+ # audio
+ if context.scene.render.ffmpeg.audio_codec == "NONE":
+ context.scene.render.ffmpeg.audio_codec = "AAC"
+ context.scene.render.ffmpeg.audio_bitrate = 192
+
+ # video
+ if self.preset == "youtube":
+ bpy.ops.script.python_file_run(
+ filepath=os.path.join(addon_directory, "render_presets", "youtube_1080.py")
+ )
+ elif self.preset == "twitter":
+ bpy.ops.script.python_file_run(
+ filepath=os.path.join(addon_directory, "render_presets", "twitter_720p.py")
+ )
+
+ from os.path import splitext, dirname
+
+ path = bpy.data.filepath
+
+ exported_file_name = "video"
+ if self.name_pattern == "blender":
+ exported_file_name = splitext(bpy.path.basename(path))[0]
+ elif self.name_pattern == "folder":
+ exported_file_name = dirname(path).rsplit(sep="\\", maxsplit=1)[-1]
+ elif self.name_pattern == "scene":
+ exported_file_name = context.scene.name
+
+ context.scene.render.filepath = "//" + exported_file_name + ".mp4"
+
+ self.report({"INFO"}, "Render settings set to the {!s} preset".format(self.preset))
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/render_presets/twitter_720p.py b/power_sequencer/operators/render_presets/twitter_720p.py
new file mode 100644
index 00000000..e7324d96
--- /dev/null
+++ b/power_sequencer/operators/render_presets/twitter_720p.py
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+# Resolution
+bpy.context.scene.render.resolution_x = 1280
+bpy.context.scene.render.resolution_y = 720
+bpy.context.scene.render.resolution_percentage = 100
+bpy.context.scene.render.pixel_aspect_x = 1
+bpy.context.scene.render.pixel_aspect_y = 1
+
+# FFMPEG
+bpy.context.scene.render.image_settings.file_format = "FFMPEG"
+bpy.context.scene.render.ffmpeg.format = "MPEG4"
+bpy.context.scene.render.ffmpeg.codec = "H264"
+
+bpy.context.scene.render.ffmpeg.constant_rate_factor = "HIGH"
+bpy.context.scene.render.ffmpeg.ffmpeg_preset = "MEDIUM"
+
+
+is_ntsc = bpy.context.scene.render.fps != 25
+if is_ntsc:
+ bpy.context.scene.render.ffmpeg.gopsize = 18
+else:
+ bpy.context.scene.render.ffmpeg.gopsize = 15
+bpy.context.scene.render.ffmpeg.use_max_b_frames = False
+
+bpy.context.scene.render.ffmpeg.video_bitrate = 4000
+bpy.context.scene.render.ffmpeg.maxrate = 4000
+bpy.context.scene.render.ffmpeg.minrate = 0
+bpy.context.scene.render.ffmpeg.buffersize = 224 * 8
+bpy.context.scene.render.ffmpeg.packetsize = 2048
+bpy.context.scene.render.ffmpeg.muxrate = 10080000
diff --git a/power_sequencer/operators/render_presets/youtube_1080.py b/power_sequencer/operators/render_presets/youtube_1080.py
new file mode 100644
index 00000000..9aa98830
--- /dev/null
+++ b/power_sequencer/operators/render_presets/youtube_1080.py
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+# Resolution
+bpy.context.scene.render.resolution_x = 1920
+bpy.context.scene.render.resolution_y = 1080
+bpy.context.scene.render.resolution_percentage = 100
+bpy.context.scene.render.pixel_aspect_x = 1
+bpy.context.scene.render.pixel_aspect_y = 1
+
+# FFMPEG
+bpy.context.scene.render.image_settings.file_format = "FFMPEG"
+bpy.context.scene.render.ffmpeg.format = "MPEG4"
+bpy.context.scene.render.ffmpeg.codec = "H264"
+
+bpy.context.scene.render.ffmpeg.constant_rate_factor = "PERC_LOSSLESS"
+bpy.context.scene.render.ffmpeg.ffmpeg_preset = "GOOD"
+
+
+is_ntsc = bpy.context.scene.render.fps != 25
+if is_ntsc:
+ bpy.context.scene.render.ffmpeg.gopsize = 18
+else:
+ bpy.context.scene.render.ffmpeg.gopsize = 15
+bpy.context.scene.render.ffmpeg.use_max_b_frames = False
+
+bpy.context.scene.render.ffmpeg.video_bitrate = 9000
+bpy.context.scene.render.ffmpeg.maxrate = 9000
+bpy.context.scene.render.ffmpeg.minrate = 0
+bpy.context.scene.render.ffmpeg.buffersize = 224 * 8
+bpy.context.scene.render.ffmpeg.packetsize = 2048
+bpy.context.scene.render.ffmpeg.muxrate = 10080000
diff --git a/power_sequencer/operators/ripple_delete.py b/power_sequencer/operators/ripple_delete.py
new file mode 100644
index 00000000..6b88a8e9
--- /dev/null
+++ b/power_sequencer/operators/ripple_delete.py
@@ -0,0 +1,111 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.doc import doc_brief, doc_description, doc_idname, doc_name
+from .utils.functions import get_frame_range
+from .utils.functions import get_mouse_frame_and_channel
+from .utils.global_settings import SequenceTypes
+from .utils.functions import slice_selection
+
+
+class POWER_SEQUENCER_OT_ripple_delete(bpy.types.Operator):
+ """
+ Delete selected strips and remove remaining gaps
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "X", "value": "PRESS", "shift": True}, {}, "Ripple Delete")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ # Auto select if no strip selected
+ frame, channel = get_mouse_frame_and_channel(context, event)
+ if not context.selected_sequences:
+ bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel)
+ if not context.selected_sequences:
+ return {"CANCELLED"}
+ return self.execute(context)
+
+ def execute(self, context):
+ scene = context.scene
+ sequencer = bpy.ops.sequencer
+ selection = context.selected_sequences
+
+ audio_scrub_active = context.scene.use_audio_scrub
+ context.scene.use_audio_scrub = False
+
+ channels = list(set([s.channel for s in selection]))
+ selection_blocks = slice_selection(context, selection)
+
+ is_single_channel = len(channels) == 1
+ if is_single_channel:
+ first_strip = selection_blocks[0][0]
+ for block in selection_blocks:
+ sequencer.select_all(action="DESELECT")
+ block_strip_start = block[0]
+ delete_start = block_strip_start.frame_final_start
+ delete_end = block[-1].frame_final_end
+ ripple_length = delete_end - delete_start
+ assert ripple_length > 0
+
+ for s in block:
+ s.select = True
+ sequencer.delete()
+
+ strips_in_channel = [
+ s
+ for s in bpy.context.sequences
+ if s.channel == channels[0]
+ and s.frame_final_start >= first_strip.frame_final_start
+ ]
+ strips_in_channel = sorted(strips_in_channel, key=attrgetter("frame_final_start"))
+ to_ripple = [s for s in strips_in_channel if s.frame_final_start > delete_start]
+ for s in to_ripple:
+ s.frame_start -= ripple_length
+
+ else:
+ for block in selection_blocks:
+ sequencer.select_all(action="DESELECT")
+ for s in block:
+ s.select = True
+ selection_start, _ = get_frame_range(context, block)
+ sequencer.delete()
+
+ scene.frame_current = selection_start
+ bpy.ops.power_sequencer.gap_remove()
+
+ self.report(
+ {"INFO"},
+ "Deleted " + str(len(selection)) + " sequence" + "s" if len(selection) > 1 else "",
+ )
+
+ context.scene.use_audio_scrub = audio_scrub_active
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/save_direct.py b/power_sequencer/operators/save_direct.py
new file mode 100644
index 00000000..f9ae5a61
--- /dev/null
+++ b/power_sequencer/operators/save_direct.py
@@ -0,0 +1,49 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_save_direct(bpy.types.Operator):
+ """
+ Saves current file without prompting for confirmation. Overrides Blender default
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "S", "value": "PRESS", "ctrl": True}, {}, "Direct Save")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene
+
+ def execute(self, context):
+ if bpy.data.is_saved:
+ bpy.ops.wm.save_mainfile()
+ else:
+ bpy.ops.wm.save_as_mainfile({"dict": "override"}, "INVOKE_DEFAULT")
+ self.report({"INFO"}, "File saved")
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/scene_create_from_selection.py b/power_sequencer/operators/scene_create_from_selection.py
new file mode 100644
index 00000000..4cf837d7
--- /dev/null
+++ b/power_sequencer/operators/scene_create_from_selection.py
@@ -0,0 +1,87 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_scene_create_from_selection(bpy.types.Operator):
+ """
+ *brief* Convert selected strips into a scene strip
+
+
+ Create a scene from the selected sequences, copying the current scene's settings, and
+ replace the selection with the newly created scene as a strip
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ move_to_first_frame: bpy.props.BoolProperty(
+ name="Move to First Frame",
+ description="The strips will start at frame 1 on the new scene",
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ start_scene_name = context.scene.name
+
+ selection = context.selected_sequences
+ selection_start_frame = min(
+ selection, key=attrgetter("frame_final_start")
+ ).frame_final_start
+ selection_start_channel = min(selection, key=attrgetter("channel")).channel
+
+ # Create new scene for the scene strip
+ bpy.ops.scene.new(type="FULL_COPY")
+ new_scene_name = context.scene.name
+
+ bpy.ops.sequencer.select_all(action="INVERT")
+ bpy.ops.power_sequencer.delete_direct()
+ frame_offset = selection_start_frame - 1
+ for s in context.sequences:
+ try:
+ s.frame_start -= frame_offset
+ except Exception:
+ continue
+ bpy.ops.sequencer.select_all()
+ bpy.ops.power_sequencer.preview_to_selection()
+
+ # Back to start scene
+ context.screen.scene = bpy.data.scenes[start_scene_name]
+
+ bpy.ops.power_sequencer.delete_direct()
+ bpy.ops.sequencer.scene_strip_add(
+ frame_start=selection_start_frame, channel=selection_start_channel, scene=new_scene_name
+ )
+ scene_strip = context.selected_sequences[0]
+ scene_strip.use_sequence = True
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/scene_cycle.py b/power_sequencer/operators/scene_cycle.py
new file mode 100644
index 00000000..f0e5cb81
--- /dev/null
+++ b/power_sequencer/operators/scene_cycle.py
@@ -0,0 +1,54 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_scene_cycle(bpy.types.Operator):
+ """
+ Cycle through scenes
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/7zhq8Tg.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "TAB", "value": "PRESS", "shift": True}, {}, "Cycle Scenes")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return bpy.data.scenes
+
+ def execute(self, context):
+ scenes = bpy.data.scenes
+
+ scene_count = len(scenes)
+
+ if context.screen.is_animation_playing:
+ bpy.ops.screen.animation_cancel(restore_frame=False)
+ for index in range(scene_count):
+ if context.scene == scenes[index]:
+ context.window.scene = scenes[(index + 1) % scene_count]
+ break
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/scene_merge_from.py b/power_sequencer/operators/scene_merge_from.py
new file mode 100644
index 00000000..cab3e353
--- /dev/null
+++ b/power_sequencer/operators/scene_merge_from.py
@@ -0,0 +1,108 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from bpy.props import BoolProperty
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_merge_from_scene_strip(bpy.types.Operator):
+ """
+ *brief* Copies all sequences and markers from a SceneStrip's scene into
+ the active scene. Optionally delete the source scene and the strip.
+
+
+ WARNING: Currently the operator doesn't recreate any animation data,
+ be careful by choosing to delete the scene after the merge.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ delete_scene = BoolProperty(
+ name="Delete Strip's scene",
+ description="Delete the SceneStrip's scene after the merging",
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor.active_strip
+
+ def invoke(self, context, event):
+ window_manager = context.window_manager
+ return window_manager.invoke_props_dialog(self)
+
+ def execute(self, context):
+ strip = context.scene.sequence_editor.active_strip
+ if strip.type != "SCENE":
+ return {'FINISHED'}
+ strip_scene = strip.scene
+ start_scene = context.window.scene
+
+ self.merge_markers(context, strip_scene, start_scene)
+ self.merge_strips(context, strip_scene, start_scene)
+
+ if not self.delete_scene:
+ return {"FINISHED"}
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip.select = True
+ bpy.ops.sequencer.delete()
+ context.window.scene = strip_scene
+ bpy.ops.scene.delete()
+ context.window.scene = start_scene
+ self.report(type={"WARNING"}, message="All animations on source scene were lost")
+
+ return {"FINISHED"}
+
+ def merge_strips(self, context, source_scene, target_scene):
+ context.window.scene = source_scene
+ bpy.ops.sequencer.select_all(action="SELECT")
+ bpy.ops.sequencer.copy()
+
+ context.window.scene = target_scene
+ current_frame = context.scene.frame_current
+ active = context.scene.sequence_editor.active_strip
+ context.scene.frame_current = active.frame_final_start
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ bpy.ops.sequencer.paste()
+
+ context.scene.frame_current = current_frame
+
+ def merge_markers(self, source_scene, target_scene):
+ if len(target_scene.timeline_markers) > 0:
+ bpy.ops.marker.select_all(action="DESELECT")
+
+ bpy.context.screen.scene = source_scene
+ bpy.ops.marker.select_all(action="SELECT")
+ bpy.ops.marker.make_links_scene(scene=target_scene.name)
+
+ bpy.context.screen.scene = target_scene
+ active = bpy.context.screen.scene.sequence_editor.active_strip
+ time_offset = active.frame_final_start
+ bpy.ops.marker.move(frames=time_offset)
+ bpy.ops.marker.select_all(action="DESELECT")
diff --git a/power_sequencer/operators/scene_open_from_strip.py b/power_sequencer/operators/scene_open_from_strip.py
new file mode 100644
index 00000000..9534855f
--- /dev/null
+++ b/power_sequencer/operators/scene_open_from_strip.py
@@ -0,0 +1,53 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_open_scene_strip(bpy.types.Operator):
+ """
+ Sets the current scene to the scene in the SceneStrip
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "E", "value": "PRESS", "alt": True, "ctrl": True}, {}, "Open Strip Scene")
+ ],
+ "keymap": "Sequencer",
+ }
+
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor.active_strip
+
+ def execute(self, context):
+ active_strip = context.scene.sequence_editor.active_strip
+ if active_strip.type != "SCENE":
+ return {'FINISHED'}
+
+ strip_scene = active_strip.scene
+ context.screen.scene = bpy.data.scenes[strip_scene.name]
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/scene_rename_with_strip.py b/power_sequencer/operators/scene_rename_with_strip.py
new file mode 100644
index 00000000..f142f8bd
--- /dev/null
+++ b/power_sequencer/operators/scene_rename_with_strip.py
@@ -0,0 +1,60 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import operator
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_scene_rename_with_strip(bpy.types.Operator):
+ """
+ Rename a Scene Strip and its source scene
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ new_name: bpy.props.StringProperty(
+ name="Strip New Name",
+ description="The name both the SceneStrip and its source Scene will take",
+ default="",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def invoke(self, context, event):
+ window_manager = context.window_manager
+ return window_manager.invoke_props_dialog(self)
+
+ def execute(self, context):
+ scene_strips = [s for s in context.selected_sequences if s.type == "SCENE"]
+ for strip in scene_strips:
+ strip.name = self.new_name
+ strip.scene.name = strip.name
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/select_all_left_or_right.py b/power_sequencer/operators/select_all_left_or_right.py
new file mode 100644
index 00000000..e92a2f7d
--- /dev/null
+++ b/power_sequencer/operators/select_all_left_or_right.py
@@ -0,0 +1,65 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_select_all_left_or_right(bpy.types.Operator):
+ """
+ *Brief* Selects all strips left or right of the time cursor.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "Q", "value": "PRESS", "shift": True},
+ {"side": "LEFT"},
+ "Select all strips to the LEFT of the time cursor",
+ ),
+ (
+ {"type": "E", "value": "PRESS", "shift": True},
+ {"side": "RIGHT"},
+ "Select all strips to the right of the time cursor",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ side: bpy.props.EnumProperty(
+ name="Side",
+ description=("Side to select"),
+ items=[
+ ("LEFT", "Left", "Move strips back in time, to the left"),
+ ("RIGHT", "Right", "Move strips forward in time, to the right"),
+ ],
+ default="LEFT",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ return bpy.ops.sequencer.select("INVOKE_DEFAULT", left_right=self.side)
diff --git a/power_sequencer/operators/select_closest_to_mouse.py b/power_sequencer/operators/select_closest_to_mouse.py
new file mode 100644
index 00000000..66860565
--- /dev/null
+++ b/power_sequencer/operators/select_closest_to_mouse.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import find_strips_mouse
+from .utils.functions import get_mouse_frame_and_channel
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_select_closest_to_mouse(bpy.types.Operator):
+ """
+ Select the closest strip under the mouse cursor
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ frame: bpy.props.IntProperty(name="Frame")
+ channel: bpy.props.IntProperty(name="Channel")
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ self.frame, self.channel = get_mouse_frame_and_channel(context, event)
+ return self.execute(context)
+
+ def execute(self, context):
+ try:
+ strip = find_strips_mouse(context, self.frame, self.channel)[0]
+ strip.select = True
+ except Exception:
+ return {"CANCELLED"}
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/select_linked_effect.py b/power_sequencer/operators/select_linked_effect.py
new file mode 100644
index 00000000..56e38276
--- /dev/null
+++ b/power_sequencer/operators/select_linked_effect.py
@@ -0,0 +1,47 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import find_linked
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_select_linked_effect(bpy.types.Operator):
+ """
+ Select all strips that are linked by an effect strip
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ for s in find_linked(context, context.sequences, context.selected_sequences):
+ s.select = True
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/select_linked_strips.py b/power_sequencer/operators/select_linked_strips.py
new file mode 100644
index 00000000..86aa8d01
--- /dev/null
+++ b/power_sequencer/operators/select_linked_strips.py
@@ -0,0 +1,70 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_select_linked_strips(bpy.types.Operator):
+ """
+ Add/Remove linked strips near mouse pointer to/from selection without the need to
+ previously have clicked/manually selected
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "L", "value": "PRESS"}, {}, "Add/Remove Linked to/from Selection")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor
+
+ def execute(self, context):
+ # save current selection first
+ selection = set(context.selected_sequences)
+
+ # if previously selected strips are linked select links as well to toggle them too
+ bpy.ops.sequencer.select_linked()
+ selection_new = set(context.selected_sequences).difference(selection)
+ # deselect & select only the linked strips near mouse pointer
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ # re-enable linked + add selection near mouse pointer
+ for s in selection_new:
+ s.select = True
+ bpy.ops.sequencer.select_linked()
+ bpy.ops.sequencer.select_linked_pick("INVOKE_DEFAULT", extend=True)
+ selection_new = set(context.selected_sequences)
+
+ # identify if linked strips under mouse pointer need to be added or removed
+ action = len(selection.intersection(selection_new)) != len(selection_new)
+
+ # re-enable previous selection
+ for s in selection:
+ s.select = True
+
+ # take care of toggle for strips under mouse
+ for s in selection_new:
+ s.select = action
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/select_related_strips.py b/power_sequencer/operators/select_related_strips.py
new file mode 100644
index 00000000..30a8b24a
--- /dev/null
+++ b/power_sequencer/operators/select_related_strips.py
@@ -0,0 +1,140 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_select_related_strips(bpy.types.Operator):
+ """
+ *brief* Find and select all strips related to the selection
+
+
+ Find and select effects related to the selection, but also inputs of selected effects.
+ This helps to then copy or duplicate strips with all attached effects.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ find_all: bpy.props.BoolProperty(
+ name="Find All",
+ description=(
+ "Find all related strips recursively so that you can copy the selection"
+ " without getting an error from Blender"
+ ),
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ if self.find_all:
+ related_strips = set()
+ for s in context.selected_sequences:
+ self.find_neighbours_recursive(related_strips, s, context)
+ else:
+ related_strips = []
+ # Only select direct neighbours and attached effects
+ effects = [s for s in context.sequences if s.type in SequenceTypes.EFFECT]
+ found_effects = self.find_related_effects(context.selected_sequences, effects)
+ related_strips.extend(found_effects)
+ while len(found_effects) > 0:
+ found_effects = self.find_related_effects(found_effects, effects)
+ related_strips.extend(found_effects)
+ for s in related_strips:
+ s.select = True
+ return {"FINISHED"}
+
+ def find_neighbours_recursive(self, visited, strip, context):
+ """
+ Performs a depth first search traversal to the graph of strips, to find
+ all related strips.
+ Args:
+ - visited: A set with all the strips that have been visited.
+ - strip: The strip to start the search from.
+ """
+ visited.add(strip)
+ neighbours = self.find_neighbours(strip, context)
+ for s in neighbours:
+ if s not in visited:
+ self.find_neighbours_recursive(visited, s, context)
+
+ def find_neighbours(self, strip, context):
+ """
+ Strips and their effect strips define a graph, where each node is a
+ strip and edges are their connections. It finds all the neighbours of a
+ strip in the graph, and *sometimes neighbours of neighbours and so on*.
+ *In order to find the neighbours of a strip the
+ bpy.ops.transform.seq_slide operator is used, and usually finds many
+ levels of neighbours, but always finds the first level, which is needed,
+ the other levels are redundant, but are included for brevity reasons.
+ Args:
+ - strip: The strip to find all its neighbours.
+ Returns: A list with all the neighbours of the strip and sometimes
+ neighbours of neighbours and so on.
+ """
+ # Respects initial selection
+ init_selected_strips = [s for s in context.selected_sequences]
+
+ neighbours = []
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip.select = True
+ bpy.ops.transform.seq_slide(value=(0, 0))
+ strip.select = False
+ for s in context.selected_sequences:
+ neighbours.append(s)
+
+ try:
+ neighbours.append(strip.input_1)
+ neighbours.append(strip.input_2)
+ except Exception:
+ pass
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in init_selected_strips:
+ s.select = True
+
+ return neighbours
+
+ def find_related_effects(self, sequences, effects):
+ found = []
+ for s in sequences:
+ for e in effects:
+ try:
+ if e.input_1 == s:
+ found.append(e)
+ except Exception:
+ continue
+ try:
+ if e.input_2 == s:
+ found.append(e)
+ except Exception:
+ continue
+ return found
diff --git a/power_sequencer/operators/select_strips_under_cursor.py b/power_sequencer/operators/select_strips_under_cursor.py
new file mode 100644
index 00000000..2fd37a60
--- /dev/null
+++ b/power_sequencer/operators/select_strips_under_cursor.py
@@ -0,0 +1,51 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_sequences_under_cursor
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_select_strips_under_cursor(bpy.types.Operator):
+ """
+ Selects the strips that are currently under the time cursor
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ deselect_first: bpy.props.BoolProperty(name="Deselect First")
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ if self.deselect_first:
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in get_sequences_under_cursor(context):
+ s.select = True
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/set_timeline_range.py b/power_sequencer/operators/set_timeline_range.py
new file mode 100644
index 00000000..7877c799
--- /dev/null
+++ b/power_sequencer/operators/set_timeline_range.py
@@ -0,0 +1,56 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_set_timeline_range(bpy.types.Operator):
+ """
+ Set the timeline start and end frame using the time cursor
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ adjust: bpy.props.EnumProperty(
+ items=[("start", "start", "start"), ("end", "end", "end")],
+ name="Adjust",
+ description="Change the start or the end frame of the timeline",
+ default="start",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor
+
+ def execute(self, context):
+ scene = context.scene
+ if self.adjust == "start":
+ scene.frame_start = scene.frame_current
+ elif self.adjust == "end":
+ scene.frame_end = scene.frame_current - 1
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/snap.py b/power_sequencer/operators/snap.py
new file mode 100644
index 00000000..29668e56
--- /dev/null
+++ b/power_sequencer/operators/snap.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_sequences_under_cursor
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_snap(bpy.types.Operator):
+ """
+ *Brief* Snaps selected strips to the time cursor ignoring locked sequences.
+
+ Automatically selects sequences if there is no active selection.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "S", "value": "PRESS", "shift": True}, {}, "Snap sequences to cursor")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ sequences = (
+ context.selected_sequences
+ if len(context.selected_sequences) > 0
+ else get_sequences_under_cursor(context)
+ )
+ frame = context.scene.frame_current
+ for s in sequences:
+ s.select = True
+ bpy.ops.sequencer.snap(frame=frame)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/snap_selection.py b/power_sequencer/operators/snap_selection.py
new file mode 100644
index 00000000..0c93b76e
--- /dev/null
+++ b/power_sequencer/operators/snap_selection.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from .utils.functions import get_sequences_under_cursor
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_snap_selection(bpy.types.Operator):
+ """
+ *Brief* Snap the entire selection to the time cursor.
+
+ Automatically selects sequences if there is no active selection.
+ To snap each strip individually, see Snap.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "S", "value": "PRESS", "alt": True}, {}, "Snap selection to cursor")
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ sequences = (
+ context.selected_sequences
+ if len(context.selected_sequences) > 0
+ else get_sequences_under_cursor(context)
+ )
+ time_move = context.scene.frame_current - sequences[0].frame_final_start
+ # bpy.ops.power_sequencer.select_related_strips()
+ for s in sequences:
+ s.frame_start += time_move
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/space_sequences.py b/power_sequencer/operators/space_sequences.py
new file mode 100644
index 00000000..3f7bb3b0
--- /dev/null
+++ b/power_sequencer/operators/space_sequences.py
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_mouse_frame_and_channel, convert_duration_to_frames
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_space_sequences(bpy.types.Operator):
+ """
+ *brief* Offsets all strips to the right of the time cursor by a given duration, ignoring locked sequences
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "EQUAL", "value": "PRESS"}, {}, "")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ gap_to_insert: bpy.props.FloatProperty(
+ name="Duration", description="The time offset to apply to the strips", default=1.0
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ sequences = [
+ s
+ for s in context.sequences
+ if s.type in SequenceTypes.CUTABLE
+ and s.frame_final_start >= context.scene.frame_current
+ and not s.lock
+ ]
+
+ gap_frames = convert_duration_to_frames(context, self.gap_to_insert)
+ sorted_sequences = sorted(sequences, key=lambda s: s.frame_final_start, reverse=True)
+ for s in sorted_sequences:
+ s.frame_start += gap_frames
+
+ markers = context.scene.timeline_markers
+ for m in [m for m in markers if m.frame >= context.scene.frame_current]:
+ m.frame += gap_frames
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/speed_remove_effect.py b/power_sequencer/operators/speed_remove_effect.py
new file mode 100644
index 00000000..d969e902
--- /dev/null
+++ b/power_sequencer/operators/speed_remove_effect.py
@@ -0,0 +1,64 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_speed_remove_effect(bpy.types.Operator):
+ """
+ *brief* Removes speed from META, un-groups META
+
+
+ This is the opposite of power_sequencer's "Add Speed" operator. It seeks out and removes
+ the speed modifier inside a meta and ungroups all the remaining strips within.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor.active_strip.type == "META"
+
+ def execute(self, context):
+ active = context.scene.sequence_editor.active_strip
+ sub_strips = []
+ for s in active.sequences:
+ if s.type == "SPEED":
+ speed_strip = s
+ else:
+ sub_strips.append(s)
+
+ bpy.ops.sequencer.meta_separate()
+ bpy.ops.sequencer.select_all(action="DESELECT")
+
+ speed_strip.select = True
+ bpy.ops.sequencer.delete()
+
+ for s in sub_strips:
+ s.select = True
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/speed_up_movie_strip.py b/power_sequencer/operators/speed_up_movie_strip.py
new file mode 100644
index 00000000..0c87b78b
--- /dev/null
+++ b/power_sequencer/operators/speed_up_movie_strip.py
@@ -0,0 +1,127 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from math import ceil
+
+from .utils.global_settings import SequenceTypes
+from .utils.functions import slice_selection
+from .utils.functions import find_linked
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_speed_up_movie_strip(bpy.types.Operator):
+ """
+ *brief* Adds a speed effect to the 2x speed, set frame end, wrap both into META
+
+ Add 2x speed to strip and set its frame end accordingly. Wraps both the strip and the speed
+ modifier into a META strip.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/ZyEd0jD.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "NUMPAD_2", "value": "PRESS", "alt": True},
+ {"speed_factor": 2.0},
+ "Speed x2",
+ ),
+ (
+ {"type": "NUMPAD_3", "value": "PRESS", "alt": True},
+ {"speed_factor": 3.0},
+ "Speed x3",
+ ),
+ (
+ {"type": "NUMPAD_4", "value": "PRESS", "alt": True},
+ {"speed_factor": 4.0},
+ "Speed x4",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ speed_factor: bpy.props.IntProperty(
+ name="Speed factor", description="How many times the footage gets sped up", default=2, min=0
+ )
+ individual_sequences: bpy.props.BoolProperty(
+ name="Affect individual strips",
+ description="Speed up every VIDEO strip individually",
+ default=False,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ sequences = [s for s in context.selected_sequences if s.type in SequenceTypes.VIDEO]
+
+ if not sequences:
+ self.report(
+ {"ERROR_INVALID_INPUT"},
+ "No Movie meta_strip or Metastrips selected. Operation cancelled",
+ )
+ return {"FINISHED"}
+
+ selection_blocks = []
+ if self.individual_sequences:
+ selection_blocks = [[s] for s in sequences]
+ else:
+ selection_blocks = slice_selection(context, sequences)
+
+ for sequences in selection_blocks:
+ self.speed_effect_add(context, sequences)
+
+ self.report(
+ {"INFO"}, "Successfully processed " + str(len(selection_blocks)) + " selection blocks"
+ )
+ return {"FINISHED"}
+
+ def speed_effect_add(self, context, sequences):
+ if not sequences:
+ return
+
+ sequence_editor = context.scene.sequence_editor
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in sequences:
+ s.select = True
+ bpy.ops.sequencer.meta_make()
+ meta_strip = sequence_editor.active_strip
+ if len(meta_strip.sequences) == 1:
+ meta_strip.sequences[0].frame_offset_start = 0
+ meta_strip.sequences[0].frame_offset_end = 0
+
+ bpy.ops.sequencer.effect_strip_add(type="SPEED")
+ speed_effect = sequence_editor.active_strip
+ speed_effect.use_default_fade = False
+ speed_effect.speed_factor = self.speed_factor
+
+ duration = ceil(meta_strip.frame_final_duration / speed_effect.speed_factor)
+ meta_strip.frame_final_end = meta_strip.frame_final_start + duration
+
+ sequence_editor.active_strip = meta_strip
+ speed_effect.select = True
+ meta_strip.select = True
+ bpy.ops.sequencer.meta_make()
+ sequence_editor.active_strip.name = (
+ meta_strip.sequences[0].name + " " + str(self.speed_factor) + "x"
+ )
diff --git a/power_sequencer/operators/swap_strips.py b/power_sequencer/operators/swap_strips.py
new file mode 100644
index 00000000..945f99ff
--- /dev/null
+++ b/power_sequencer/operators/swap_strips.py
@@ -0,0 +1,247 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_swap_strips(bpy.types.Operator):
+ """
+ *brief* Swaps the 2 strips between them
+
+
+ Places the first strip in the channel and starting frame (frame_final_start) of the second
+ strip, and places the second strip in the channel and starting frame (frame_final_end) of
+ the first strip. If there is no space for the swap, it does nothing.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ direction: bpy.props.EnumProperty(
+ name="Direction",
+ description="The direction to find the closest strip",
+ items=[
+ ("up", "Up", "The direction up from the selected strip"),
+ ("down", "Down", "The direction down from the selected strip"),
+ ],
+ default="up",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ strip_1 = context.selected_sequences[0]
+ if len(context.selected_sequences) == 1:
+ strip_2 = self.find_closest_strip_vertical(context, strip_1, self.direction)
+ else:
+ strip_2 = context.selected_sequences[1]
+ if not strip_2 or strip_1.lock or strip_2.lock:
+ return {"CANCELLED"}
+
+ # Swap a strip and one of its effects
+ if hasattr(strip_1, "input_1") or hasattr(strip_2, "input_1"):
+ if not self.are_linked(strip_1, strip_2):
+ return {"CANCELLED"}
+ self.swap_with_effect(strip_1, strip_2)
+ return {"FINISHED"}
+
+ s1_start, s1_channel = strip_1.frame_final_start, strip_1.channel
+ s2_start, s2_channel = strip_2.frame_final_start, strip_2.channel
+
+ self.move_to_end(strip_1, context)
+ self.move_to_end(strip_2, context)
+
+ s1_start_2 = strip_1.frame_final_start
+ s2_start_2 = strip_2.frame_final_start
+
+ group_1 = {
+ s: s.channel
+ for s in context.sequences
+ if s.frame_final_start == s1_start_2 and s != strip_1
+ }
+ group_2 = {
+ s: s.channel
+ for s in context.sequences
+ if s.frame_final_start == s2_start_2 and s != strip_2
+ }
+
+ strip_2.select = False
+ bpy.ops.transform.seq_slide(
+ value=(s2_start - strip_1.frame_final_start, s2_channel - strip_1.channel)
+ )
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip_2.select = True
+ bpy.ops.transform.seq_slide(
+ value=(s1_start - strip_2.frame_final_start, s1_channel - strip_2.channel)
+ )
+
+ if not self.fits(
+ strip_1, group_1, s2_start, s1_channel, s2_channel, context
+ ) or not self.fits(strip_2, group_2, s1_start, s2_channel, s1_channel, context):
+ self.reconstruct(strip_1, s1_channel, group_1, context)
+ self.reconstruct(strip_2, s2_channel, group_2, context)
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip_1.select = True
+ bpy.ops.transform.seq_slide(
+ value=(s1_start - strip_1.frame_final_start, s1_channel - strip_1.channel)
+ )
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip_2.select = True
+ bpy.ops.transform.seq_slide(
+ value=(s2_start - strip_2.frame_final_start, s2_channel - strip_2.channel)
+ )
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip_1.select = True
+ strip_2.select = True
+ return {"CANCELLED"}
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip_1.select = True
+ strip_2.select = True
+ return {"FINISHED"}
+
+ def move_to_frame(self, strip, frame, context):
+ """
+ Moves a strip based on its frame_final_start without changing its
+ duration.
+ Args:
+ - strip: The strip to be moved.
+ - frame: The frame, the frame_final_start of the strip will be placed
+ at.
+ """
+ selected_strips = context.selected_sequences
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip.select = True
+
+ bpy.ops.transform.seq_slide(value=(frame - strip.frame_final_start, 0))
+
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in selected_strips:
+ s.select = True
+
+ def move_to_end(self, strip, context):
+ """
+ Moves a strip to an empty slot at the end of the sequencer, different
+ than its initial slot.
+ Args:
+ - strip: The strip to move.
+ """
+ end_frame = max(context.sequences, key=attrgetter("frame_final_end")).frame_final_end
+ self.move_to_frame(strip, end_frame, context)
+
+ def fits(self, strip, group, frame, init_channel, target_channel, context):
+ """
+ Checks if a swap has been successful or not.
+ Args:
+ - strip: The core strip of the swap.
+ - group: The effect strips of the core strip.
+ - frame: The starting frame of the target location.
+ - init_channel: The initial channel of the strip, before the swap took
+ place.
+ - target_channel: The channel of the target location.
+ Returns: True if the swap was successful, otherwise False.
+ """
+ if strip.frame_final_start != frame or strip.channel != target_channel:
+ return False
+
+ offset = strip.channel - init_channel
+ for s in group.keys():
+ if s.channel != group[s] + offset:
+ return False
+
+ return context.selected_sequences
+
+ def reconstruct(self, strip, init_channel, group, context):
+ """
+ Reconstructs a failed swap, based on a core strip. After its done, the
+ core strip is placed at the end of the sequencer, in an empty slot.
+ Args:
+ - strip: The core strip of the swap.
+ - init_channel: The initial channel of the core strip.
+ - group: A dictionary with the effect strips of the core strip, and
+ their target channels.
+ """
+ self.move_to_end(strip, context)
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ strip.select = True
+ bpy.ops.transform.seq_slide(value=(0, init_channel - strip.channel))
+
+ for s in group.keys():
+ channel = group[s]
+ for u in group.keys():
+ if u.channel == channel and u != s:
+ u.channel += 1
+ s.channel = channel
+
+ def find_closest_strip_vertical(self, context, strip, direction):
+ """
+ Finds the closest strip to a given strip in a specific direction.
+ Args:
+ - strip: The base strip.
+ Returns: The closest strip to the given strip, in the proper direction.
+ If no strip is found, returns None.
+ """
+ strips_in_range = (
+ s
+ for s in context.sequences
+ if strip.frame_final_start <= s.frame_final_start
+ and s.frame_final_end <= strip.frame_final_end
+ )
+ if direction == "up":
+ strips_above = [s for s in strips_in_range if s.channel > strip.channel]
+ if not strips_above:
+ return
+ return min(strips_above, key=attrgetter("channel"))
+ elif direction == "down":
+ strips_below = [s for s in strips_in_range if s.channel < strip.channel]
+ if not strips_below:
+ return
+ return max(strips_below, key=attrgetter("channel"))
+
+ def are_linked(self, strip_1, strip_2):
+ return (
+ strip_1.frame_final_start == strip_2.frame_final_start
+ and strip_1.frame_final_end == strip_2.frame_final_end
+ )
+
+ def swap_with_effect(self, strip_1, strip_2):
+ effect_strip = strip_1 if hasattr(strip_1, "input_1") else strip_2
+ other_strip = strip_1 if effect_strip != strip_1 else strip_2
+
+ effect_strip_channel = effect_strip.channel
+ other_strip_channel = other_strip.channel
+
+ effect_strip.channel -= 1
+ other_strip.channel = effect_strip_channel
+ effect_strip.channel = other_strip_channel
diff --git a/power_sequencer/operators/synchronize_titles.py b/power_sequencer/operators/synchronize_titles.py
new file mode 100644
index 00000000..3df342f4
--- /dev/null
+++ b/power_sequencer/operators/synchronize_titles.py
@@ -0,0 +1,112 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+# TODO: rewrite to sync strips to corresponding identifiers instead
+# See https://github.com/GDquest/Blender-power-sequencer/issues/55
+class POWER_SEQUENCER_OT_synchronize_titles(bpy.types.Operator):
+ """
+ *brief* Snap the selected image or text strips to the corresponding title marker
+
+
+ The marker and strip names have to start with TITLE-001
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ TITLE_REGEX = r"^TITLE-?([0-9]+)-?"
+
+ @classmethod
+ def poll(cls, context):
+ return context.scene.sequence_editor
+
+ def execute(self, context):
+ markers = context.scene.timeline_markers
+ selection = context.selected_sequences
+
+ if not markers and selection:
+ if not markers:
+ self.report({"INFO"}, "No markers, operation cancelled.")
+ else:
+ self.report({"INFO"}, "No sequences selected, operation cancelled.")
+ return {"CANCELLED"}
+
+ title_markers = self.find_markers(context, self.TITLE_REGEX)
+ if not title_markers:
+ self.report({"INFO"}, "No title markers found, operation cancelled.")
+
+ matched = self.match_sequences_and_markers(selection, title_markers, self.TITLE_REGEX)
+ for s, m in matched:
+ s.frame_start = m.frame
+ return {"FINISHED"}
+
+ def match_sequences_and_markers(self, sequences, markers, regex):
+ """Takes a list of sequences, of markers, and checks if they both
+ match a regular expression.
+ Returns a list of pairs of sequence and marker as tuples
+
+ Args:
+ - sequences, the list of sequences
+ - markers, a list of markers
+ - regex, the regular expression to match"""
+ if not sequences and markers and regex:
+ raise AttributeError("missing attributes")
+
+ import re
+ from .utils.global_settings import SequenceTypes
+
+ sequences = (s for s in sequences if s.type not in SequenceTypes.EFFECT)
+
+ return_list = []
+ re_title = re.compile(regex)
+ for s in sequences:
+ found = re_title.match(s.name)
+ title_id = int(found.group(1)) if found else None
+ for m in markers:
+ found = re_title.match(m.name)
+ marker_id = int(found.group(1)) if found else None
+ if marker_id == title_id:
+ return_list.append((s, m))
+ break
+ return return_list
+
+ def find_markers(self, context, regex):
+ """Finds and returns all markers using REGEX
+ Args:
+ - regex, the re match pattern to use"""
+ if not regex:
+ raise AttributeError("regex parameter missing")
+
+ import re
+
+ regex = re.compile(regex)
+ markers = context.scene.timeline_markers
+ markers = (m for m in markers if regex.match(m.name))
+ return markers
diff --git a/power_sequencer/operators/toggle_selected_mute.py b/power_sequencer/operators/toggle_selected_mute.py
new file mode 100644
index 00000000..cd445205
--- /dev/null
+++ b/power_sequencer/operators/toggle_selected_mute.py
@@ -0,0 +1,71 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_toggle_selected_mute(bpy.types.Operator):
+ """
+ Mute or unmute selected sequences
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "H", "value": "PRESS"},
+ {"use_unselected": False},
+ "Mute or Unmute Selected Strips",
+ ),
+ (
+ {"type": "H", "value": "PRESS", "alt": True},
+ {"use_unselected": True},
+ "Mute or Unmute Selected Strips",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ use_unselected: bpy.props.BoolProperty(
+ name="Use unselected", description="Toggle non selected sequences", default=False
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ selection = context.selected_sequences
+
+ if self.use_unselected:
+ selection = [s for s in context.sequences if s not in selection]
+
+ if not selection:
+ self.report({"WARNING"}, "No sequences to toggle muted")
+ return {"CANCELLED"}
+
+ mute = not selection[0].mute
+ for s in selection:
+ s.mute = mute
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/toggle_waveforms.py b/power_sequencer/operators/toggle_waveforms.py
new file mode 100644
index 00000000..c3e45980
--- /dev/null
+++ b/power_sequencer/operators/toggle_waveforms.py
@@ -0,0 +1,82 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_toggle_waveforms(bpy.types.Operator):
+ """
+ *brief* Toggle audio waveforms
+
+ Toggle drawing of waveforms for selected strips or for all audio strips if no selection
+ is active.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "https://i.imgur.com/HJ5ryhv.gif",
+ "description": doc_description(__doc__),
+ "shortcuts": [({"type": "W", "value": "PRESS", "alt": True}, {}, "Toggle Waveforms")],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ mode: bpy.props.EnumProperty(
+ items=[
+ ("auto", "Auto", "Automatically toggle the waveform"),
+ ("on", "On", "Make the waveforms visible"),
+ ("off", "Off", "Make the waveforms invisible"),
+ ],
+ name="Waveform visibility",
+ description="Force the waveforms' visibility with On or Off, \
+ or let Blender choose automatically",
+ default="auto",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+ def execute(self, context):
+ selection = context.selected_sequences
+ if not selection:
+ selection = context.sequences
+
+ sequences = [s for s in selection if s.type in SequenceTypes.SOUND]
+
+ if not sequences:
+ self.report({"ERROR_INVALID_INPUT"}, "Select at least one sound strip")
+ return {"CANCELLED"}
+
+ show_waveform = None
+ if self.mode == "auto":
+ from operator import attrgetter
+
+ show_waveform = not sorted(sequences, key=attrgetter("frame_final_start"))[
+ 0
+ ].show_waveform
+ else:
+ show_waveform = True if self.mode == "on" else False
+
+ for s in sequences:
+ s.show_waveform = show_waveform
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/transitions_remove.py b/power_sequencer/operators/transitions_remove.py
new file mode 100644
index 00000000..aabb2af5
--- /dev/null
+++ b/power_sequencer/operators/transitions_remove.py
@@ -0,0 +1,85 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from operator import attrgetter
+
+from .utils.global_settings import SequenceTypes
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_transitions_remove(bpy.types.Operator):
+ """
+ Delete a crossfade strip and moves the handles of the input strips to form a cut again
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ sequences_override = []
+
+ @classmethod
+ def poll(cls, context):
+ return context.selected_sequences
+
+
+ def execute(self, context):
+ to_process = (
+ self.sequences_override if self.sequences_override else context.selected_sequences
+ )
+
+ transitions = [s for s in to_process if s.type in SequenceTypes.TRANSITION]
+ if not transitions:
+ return {"FINISHED"}
+
+ saved_selection = [
+ s for s in context.selected_sequences if s.type not in SequenceTypes.TRANSITION
+ ]
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for transition in transitions:
+ effect_middle_frame = round(
+ (transition.frame_final_start + transition.frame_final_end) / 2
+ )
+
+ inputs = [transition.input_1, transition.input_2]
+ strips_to_edit = []
+ for input in inputs:
+ if input.type in SequenceTypes.EFFECT and hasattr(input, "input_1"):
+ strips_to_edit.append(input.input_1)
+ else:
+ strips_to_edit.append(input)
+
+ strip_1 = min(strips_to_edit, key=attrgetter("frame_final_end"))
+ strip_2 = max(strips_to_edit, key=attrgetter("frame_final_end"))
+
+ strip_1.frame_final_end = effect_middle_frame
+ strip_2.frame_final_start = effect_middle_frame
+
+ transition.select = True
+ bpy.ops.sequencer.delete()
+
+ for s in saved_selection:
+ s.select = True
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/trim_left_or_right_handles.py b/power_sequencer/operators/trim_left_or_right_handles.py
new file mode 100644
index 00000000..75a461da
--- /dev/null
+++ b/power_sequencer/operators/trim_left_or_right_handles.py
@@ -0,0 +1,115 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+from operator import attrgetter
+
+import bpy
+
+from .utils.doc import doc_brief, doc_description, doc_idname, doc_name
+
+
+class POWER_SEQUENCER_OT_trim_left_or_right_handles(bpy.types.Operator):
+ """
+ Trims or extends the handle closest to the time cursor for all selected strips.
+
+ If you keep the Shift key down, the edit will ripple through the timeline.
+ Auto selects sequences under the time cursor when you don't have a selection.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "K", "value": "PRESS", "alt": True},
+ {"side": "RIGHT", "ripple": False},
+ "Smart Snap Right",
+ ),
+ (
+ {"type": "K", "value": "PRESS", "alt": True, "shift": True},
+ {"side": "RIGHT", "ripple": True},
+ "Smart Snap Right With Ripple",
+ ),
+ (
+ {"type": "K", "value": "PRESS", "ctrl": True},
+ {"side": "LEFT", "ripple": False},
+ "Smart Snap Left",
+ ),
+ (
+ {"type": "K", "value": "PRESS", "ctrl": True, "shift": True},
+ {"side": "LEFT", "ripple": True},
+ "Smart Snap Left With Ripple",
+ ),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ side: bpy.props.EnumProperty(
+ items=[("LEFT", "Left", "Left side"), ("RIGHT", "Right", "Right side")],
+ name="Snap side",
+ description="Handle side to use for the snap",
+ default="LEFT",
+ )
+ ripple: bpy.props.BoolProperty(name="Ripple", default=False)
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def execute(self, context):
+ frame_current = context.scene.frame_current
+
+ # Only select sequences under the time cursor
+ sequences = context.selected_sequences if context.selected_sequences else context.sequences
+ for s in sequences:
+ s.select = s.frame_final_start <= frame_current and s.frame_final_end >= frame_current
+ sequences = [s for s in sequences if s.select]
+ if not sequences:
+ return {"FINISHED"}
+
+ for s in sequences:
+ if self.side == "LEFT":
+ s.select_left_handle = True
+ if self.side == "RIGHT":
+ s.select_right_handle = True
+
+ # If trimming from the left, we need to save the start frame before trimming
+ ripple_start_frame = 0
+ if self.ripple and self.side == "LEFT":
+ ripple_start_frame = min(
+ sequences, key=attrgetter("frame_final_start")
+ ).frame_final_start
+
+ bpy.ops.sequencer.snap(frame=frame_current)
+ for s in sequences:
+ s.select_right_handle = False
+ s.select_left_handle = False
+
+ if self.ripple and sequences:
+ if self.side == "RIGHT":
+ ripple_start_frame = max(
+ sequences, key=attrgetter("frame_final_end")
+ ).frame_final_end
+ bpy.ops.power_sequencer.gap_remove(frame=ripple_start_frame)
+ else:
+ bpy.ops.power_sequencer.gap_remove(frame=ripple_start_frame)
+
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/trim_three_point_edit.py b/power_sequencer/operators/trim_three_point_edit.py
new file mode 100644
index 00000000..aa4d888f
--- /dev/null
+++ b/power_sequencer/operators/trim_three_point_edit.py
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+from .utils.functions import get_mouse_frame_and_channel
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+
+
+class POWER_SEQUENCER_OT_trim_three_point_edit(bpy.types.Operator):
+ """
+ Trim the closest strip under the mouse cursor in or out
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ ({"type": "I", "value": "PRESS"}, {"side": "LEFT"}, "Trim In"),
+ ({"type": "O", "value": "PRESS"}, {"side": "RIGHT"}, "Trim Out"),
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ side: bpy.props.EnumProperty(
+ items=[("LEFT", "Left", "Left side"), ("RIGHT", "Right", "Right side")],
+ name="Trim side",
+ description="Side of the strip(s) to trim, either LEFT or RIGHT",
+ default="LEFT",
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context.sequences
+
+ def invoke(self, context, event):
+ frame, channel = get_mouse_frame_and_channel(context, event)
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ bpy.ops.power_sequencer.select_closest_to_mouse(frame=frame, channel=channel)
+ if not context.selected_sequences:
+ bpy.ops.power_sequencer.select_strips_under_cursor()
+ return self.execute(context)
+
+ def execute(self, context):
+ if not context.selected_sequences:
+ return {"CANCELLED"}
+ bpy.ops.power_sequencer.trim_left_or_right_handles(side=self.side)
+ return {"FINISHED"}
diff --git a/power_sequencer/operators/trim_to_surrounding_cuts.py b/power_sequencer/operators/trim_to_surrounding_cuts.py
new file mode 100644
index 00000000..3f98cc8c
--- /dev/null
+++ b/power_sequencer/operators/trim_to_surrounding_cuts.py
@@ -0,0 +1,164 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Find the two closest cuts, trims and deletes all strips above in the range but leaves some
+margin. Removes the newly formed gap.
+"""
+import bpy
+from math import floor
+
+from .utils.functions import convert_duration_to_frames
+from .utils.doc import doc_name, doc_idname, doc_brief, doc_description
+from .utils.functions import (
+ find_closest_surrounding_cuts_frames,
+ sequencer_workaround_2_80_audio_bug,
+)
+
+
+class POWER_SEQUENCER_OT_trim_to_surrounding_cuts(bpy.types.Operator):
+ """
+ *Brief* Automatically trim to surrounding cuts with some time offset
+
+ Finds the two cuts closest to the mouse cursor and trims the footage in between, leaving a little time offset. It's useful after you removed some bad audio but you need to keep some video around for a transition.
+ By default, the tool leaves 0.2 a seconds margin on either side of the trim.
+ """
+
+ doc = {
+ "name": doc_name(__qualname__),
+ "demo": "",
+ "description": doc_description(__doc__),
+ "shortcuts": [
+ (
+ {"type": "LEFTMOUSE", "value": "PRESS", "shift": True, "alt": True},
+ {},
+ "Trim to Surrounding Cuts",
+ )
+ ],
+ "keymap": "Sequencer",
+ }
+ bl_idname = doc_idname(__qualname__)
+ bl_label = doc["name"]
+ bl_description = doc_brief(doc["description"])
+ bl_options = {"REGISTER", "UNDO"}
+
+ margin: bpy.props.FloatProperty(
+ name="Trim margin",
+ description="Margin to leave on either sides of the trim in seconds",
+ default=0.2,
+ min=0,
+ )
+ gap_remove: bpy.props.BoolProperty(
+ name="Remove gaps",
+ description="When trimming the sequences, remove gaps automatically",
+ default=True,
+ )
+
+ @classmethod
+ def poll(cls, context):
+ return context
+
+ def invoke(self, context, event):
+ if not context.sequences:
+ return {"CANCELLED"}
+
+ sequencer = bpy.ops.sequencer
+
+ # Convert mouse position to frame, channel
+ x = context.region.view2d.region_to_view(x=event.mouse_region_x, y=event.mouse_region_y)[0]
+ frame = round(x)
+
+ left_cut_frame, right_cut_frame = find_closest_surrounding_cuts_frames(context, frame)
+ surrounding_cut_frames_duration = abs(left_cut_frame - right_cut_frame)
+
+ margin_frame = convert_duration_to_frames(context, self.margin)
+
+ if surrounding_cut_frames_duration <= margin_frame * 2:
+ self.report(
+ {"WARNING"},
+ ("The trim margin is larger than the gap\n" "Use snap trim or reduce the margin"),
+ )
+ return {"CANCELLED"}
+
+ to_delete, to_trim = self.find_strips_in_range(context, left_cut_frame, right_cut_frame)
+ trim_start, trim_end = (left_cut_frame + margin_frame, right_cut_frame - margin_frame)
+
+ for s in to_trim:
+ # If the strip is larger than the range to trim cut it in three
+ if s.frame_final_start < trim_start and s.frame_final_end > trim_end:
+ sequencer.select_all(action="DESELECT")
+ s.select = True
+ sequencer.cut(frame=trim_start, type="SOFT", side="RIGHT")
+ sequencer.cut(frame=trim_end, type="SOFT", side="LEFT")
+ to_delete.append(context.selected_sequences[0])
+ continue
+
+ if s.frame_final_start < trim_end and s.frame_final_end > trim_end:
+ s.frame_final_start = trim_end
+ elif s.frame_final_end > trim_start and s.frame_final_start < trim_start:
+ s.frame_final_end = trim_start
+
+ # Delete all sequences that are between the cuts
+ sequencer.select_all(action="DESELECT")
+ for s in to_delete:
+ s.select = True
+ sequencer.delete()
+
+ if self.gap_remove:
+ frame_to_remove_gap = right_cut_frame - 1 if frame == right_cut_frame else frame
+ # bpy.ops.anim.change_frame(frame_to_remove_gap)
+ context.scene.frame_current = frame_to_remove_gap
+ bpy.ops.power_sequencer.gap_remove()
+ context.scene.frame_current = trim_start
+
+ # FIXME: Workaround Blender 2.80's audio bug, remove when fixed in Blender
+ sequencer_workaround_2_80_audio_bug(context)
+
+ return {"FINISHED"}
+
+ def find_strips_in_range(
+ self, context, start_frame, end_frame, sequences=None, find_overlapping=True
+ ):
+ """
+ Returns strips which start and end within a certain frame range, or that overlap a
+ certain frame range
+ Args:
+ - start_frame, the start of the frame range
+ - end_frame, the end of the frame range
+ - sequences (optional): only work with these sequences.
+ If it doesn't receive any, the function works with all the sequences in the current context
+ - find_overlapping (optional): find and return a list of strips that overlap the
+ frame range
+
+ Returns a tuple of two lists:
+ [0], strips entirely in the frame range
+ [1], strips that only overlap the frame range
+ """
+ strips_in_range = []
+ strips_overlapping_range = []
+ if not sequences:
+ sequences = context.sequences
+ for s in sequences:
+ if start_frame <= s.frame_final_start <= end_frame:
+ if start_frame <= s.frame_final_end <= end_frame:
+ strips_in_range.append(s)
+ elif find_overlapping:
+ strips_overlapping_range.append(s)
+ elif find_overlapping and start_frame <= s.frame_final_end <= end_frame:
+ strips_overlapping_range.append(s)
+ if s.frame_final_start < start_frame and s.frame_final_end > end_frame:
+ strips_overlapping_range.append(s)
+ return strips_in_range, strips_overlapping_range
diff --git a/power_sequencer/operators/utils/__init__.py b/power_sequencer/operators/utils/__init__.py
new file mode 100644
index 00000000..ad15033c
--- /dev/null
+++ b/power_sequencer/operators/utils/__init__.py
@@ -0,0 +1,16 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
diff --git a/power_sequencer/operators/utils/doc.py b/power_sequencer/operators/utils/doc.py
new file mode 100644
index 00000000..82a87581
--- /dev/null
+++ b/power_sequencer/operators/utils/doc.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Utilities to convert operator names and docstrings to human-readable text.
+Used to generate names for Blender's operator search, and to generate Power Sequencer's documentation.
+"""
+import re
+
+
+upper_match = lambda m: m.string
+
+
+def doc_idname(s):
+ """
+ Returns the id_name of the operator to register shortcuts in Blender's keymaps or call from other operators.
+ """
+ out = ".".join(map(str.lower, s.split("_OT_")))
+ return out
+
+
+def doc_name(s):
+ """
+ Returns the operator's name in a human readable format for Blender's operator search.
+ Removes POWER_SEQUENCER_OT_ from an operator's identifier
+ and converts it to title case.
+ """
+ out = s.split("_OT")[-1]
+ out = out.replace("_", " ").lstrip().title()
+ return out
+
+
+def doc_brief(s):
+ """
+ Returns the first line of an operator's docstring to use as a summary of how the operator works.
+ The line in question must contain *brief*.
+ """
+ return " ".join(s.split("\n\n")[0].split()[1:]) if s.startswith("*brief*") else s
+
+
+def doc_description(s):
+ """
+ Returns the lines after the brief line in an operator's documentation strings. See doc_brief above.
+ """
+ return "\n".join(map(lambda x: x.strip(), s.split("\n"))).strip()
diff --git a/power_sequencer/operators/utils/draw.py b/power_sequencer/operators/utils/draw.py
new file mode 100644
index 00000000..0d993a53
--- /dev/null
+++ b/power_sequencer/operators/utils/draw.py
@@ -0,0 +1,121 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""Drawing utilities. A list of functions to draw common elements"""
+# import bgl
+import blf
+from gpu_extras.batch import batch_for_shader
+from mathutils import Vector
+import math
+
+
+def get_color_gizmo_primary(context):
+ color = context.preferences.themes[0].user_interface.gizmo_primary
+ return _color_to_list(color)
+
+
+def get_color_gizmo_secondary(context):
+ color = context.preferences.themes[0].user_interface.gizmo_secondary
+ return _color_to_list(color)
+
+
+def get_color_axis_x(context):
+ color = context.preferences.themes[0].user_interface.axis_x
+ return _color_to_list(color)
+
+
+def get_color_axis_y(context):
+ color = context.preferences.themes[0].user_interface.axis_y
+ return _color_to_list(color)
+
+
+def get_color_axis_z(context):
+ color = context.preferences.themes[0].user_interface.axis_z
+ return _color_to_list(color)
+
+
+def draw_line(shader, start, end, color=(1.0, 1.0, 1.0, 1.0)):
+ """Draws a line using two Vector-based points"""
+ batch = batch_for_shader(shader, "LINES", {"pos": (start, end)})
+
+ shader.bind()
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+
+def draw_rectangle(shader, origin, size, color=(1.0, 1.0, 1.0, 1.0)):
+ vertices = (
+ (origin.x, origin.y),
+ (origin.x + size.x, origin.y),
+ (origin.x, origin.y + size.y),
+ (origin.x + size.x, origin.y + size.y),
+ )
+ indices = ((0, 1, 2), (2, 1, 3))
+ batch = batch_for_shader(shader, "TRIS", {"pos": vertices}, indices=indices)
+ shader.bind()
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+
+def draw_triangle(shader, point_1, point_2, point_3, color=(1.0, 1.0, 1.0, 1.0)):
+ vertices = (point_1, point_2, point_3)
+ indices = ((0, 1, 2),)
+ batch = batch_for_shader(shader, "TRIS", {"pos": vertices}, indices=indices)
+ shader.bind()
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+
+def draw_triangle_equilateral(shader, center, radius, rotation=0.0, color=(1.0, 1.0, 1.0, 1.0)):
+ points = []
+ for i in range(3):
+ angle = i * math.pi * 2 / 3 + rotation
+ offset = Vector((radius * math.cos(angle), radius * math.sin(angle)))
+ points.append(center + offset)
+ draw_triangle(shader, *points, color)
+
+
+def draw_text(x, y, size, text, justify="left", color=(1.0, 1.0, 1.0, 1.0)):
+ font_id = 0
+ blf.color(font_id, *color)
+ if justify == "right":
+ text_width, text_height = blf.dimensions(font_id, text)
+ else:
+ text_width = 0
+ blf.position(font_id, x - text_width, y, 0)
+ blf.size(font_id, size, 72)
+ blf.draw(font_id, text)
+
+
+def draw_arrow_head(shader, center, size, points_right=True, color=(1.0, 1.0, 1.0, 1.0)):
+ """
+ Draws a triangular arrow using two Vectors:
+ - the triangle's center
+ - the triangle's size
+ """
+ direction = 1 if points_right else -1
+
+ point_upper = Vector([center.x - size.x / 2 * direction, center.y + size.y / 2])
+ point_tip = Vector([center.x + size.x / 2 * direction, center.y])
+ point_lower = Vector([center.x - size.x / 2 * direction, center.y - size.y / 2])
+
+ draw_line(shader, point_upper, point_tip, color)
+ draw_line(shader, point_tip, point_lower, color)
+
+
+def _color_to_list(color):
+ """Converts a Blender Color to a list of 4 color values to use with shaders and drawing"""
+ return list(color) + [1.0]
diff --git a/power_sequencer/operators/utils/functions.py b/power_sequencer/operators/utils/functions.py
new file mode 100644
index 00000000..35fd900d
--- /dev/null
+++ b/power_sequencer/operators/utils/functions.py
@@ -0,0 +1,343 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import subprocess
+from math import sqrt, floor
+from operator import attrgetter
+from .global_settings import SequenceTypes
+
+
+def calculate_distance(x1, y1, x2, y2):
+ return sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
+
+
+def convert_duration_to_frames(context, duration):
+ return round(duration * context.scene.render.fps / context.scene.render.fps_base)
+
+
+def find_linked(context, sequences, selected_sequences):
+ """
+ Takes a list of sequences and returns a list of all the sequences
+ and effects that are linked in time
+
+ Args:
+ - sequences: a list of sequences
+
+ Returns a list of all the linked sequences, but not the sequences passed to the function
+ """
+ start, end = get_frame_range(context, sequences, selected_sequences)
+ sequences_in_range = [s for s in sequences if is_in_range(context, s, start, end)]
+ effects = (s for s in sequences_in_range if s.type in SequenceTypes.EFFECT)
+ selected_effects = (s for s in sequences if s.type in SequenceTypes.EFFECT)
+
+ linked_sequences = []
+
+ # Filter down to effects that have at least one of seq as input and
+ # Append input sequences that aren't in the source list to linked_sequences
+ for e in effects:
+ if not hasattr(e, "input_2"):
+ continue
+ for s in sequences:
+ if e.input_2 == s:
+ linked_sequences.append(e)
+ if e.input_1 not in sequences:
+ linked_sequences.append(e.input_1)
+ elif e.input_1 == s:
+ linked_sequences.append(e)
+ if e.input_2 not in sequences:
+ linked_sequences.append(e.input_2)
+
+ # Find inputs of selected effects that are not selected
+ for e in selected_effects:
+ try:
+ if e.input_1 not in sequences:
+ linked_sequences.append(e.input_1)
+ if e.input_count == 2:
+ if e.input_2 not in sequences:
+ linked_sequences.append(e.input_2)
+ except AttributeError:
+ continue
+
+ return linked_sequences
+
+
+def find_neighboring_markers(context, frame=None):
+ """Returns a tuple containing the closest marker to the left and to the right of the frame"""
+ markers = context.scene.timeline_markers
+
+ if not (frame and markers):
+ return None, None
+
+ markers = sorted(markers, key=attrgetter("frame"))
+
+ previous_marker, next_marker = None, None
+ for m in markers:
+ previous_marker = m if m.frame < frame else previous_marker
+ if m.frame > frame:
+ next_marker = m
+ break
+
+ return previous_marker, next_marker
+
+
+def find_sequences_after(context, sequence):
+ """
+ Finds the strips following the sequences passed to the function
+ Args:
+ - Sequences, the sequences to check
+ Returns all the strips after the sequence in the current context
+ """
+ return [s for s in context.sequences if s.frame_final_start > sequence.frame_final_start]
+
+
+def find_snap_candidate(context, frame=0):
+ """
+ Returns the cut frame closest to the `frame` argument
+ """
+ snap_candidate = 1000000
+
+ for s in context.sequences:
+ start_to_frame = frame - s.frame_final_start
+ end_to_frame = frame - s.frame_final_end
+
+ distance_to_start = abs(start_to_frame)
+ distance_to_end = abs(end_to_frame)
+
+ candidate = (
+ frame - start_to_frame
+ if min(distance_to_start, distance_to_end) == distance_to_start
+ else frame - end_to_frame
+ )
+
+ if abs(frame - candidate) < abs(frame - snap_candidate):
+ snap_candidate = candidate
+
+ return snap_candidate
+
+
+def find_strips_mouse(context, frame, channel, select_linked=False):
+ """
+ Finds a list of sequences to select based on the frame and channel the mouse cursor is at
+
+ Args:
+ - frame: the frame the mouse or cursor is on
+ - channel: the channel the mouse is hovering
+ - select_linked: find and append the sequences linked in time if True
+
+ Returns the sequence(s) under the mouse cursor as a list
+ Returns an empty list if nothing found
+ """
+ sequences = [
+ s
+ for s in context.sequences
+ if not s.lock and s.channel == channel and s.frame_final_start <= frame <= s.frame_final_end
+ ]
+ if select_linked:
+ linked_strips = [
+ s
+ for s in context.sequences
+ if s.frame_final_start == sequences[0].frame_final_start
+ and s.frame_final_end == sequences[0].frame_final_end
+ ]
+ sequences.extend(linked_strips)
+ return sequences
+
+
+def get_frame_range(context, sequences=[], get_from_start=False):
+ """
+ Returns a tuple with the minimum and maximum frames of the
+ list of passed sequences.
+ Args:
+ - sequences, the sequences to use
+ - get_from_start, the returned start frame is set to 1 if
+ this boolean is True
+ """
+ start, end = -1, -1
+ start = (
+ 1
+ if get_from_start
+ else min(sequences, key=attrgetter("frame_final_start")).frame_final_start
+ )
+ end = max(sequences, key=attrgetter("frame_final_end")).frame_final_end
+ return start, end
+
+
+def get_mouse_frame_and_channel(context, event):
+ """
+ Convert mouse coordinates from the event, from
+ pixels to frame, channel.
+ Returns a tuple of frame, channel as integers
+ """
+ view2d = context.region.view2d
+ frame, channel = view2d.region_to_view(event.mouse_region_x, event.mouse_region_y)
+ return round(frame), floor(channel)
+
+
+def is_in_range(context, sequence, start, end):
+ """
+ Checks if a single sequence's start or end is in the range
+
+ Args:
+ - sequence: the sequence to check for
+ - start, end: the start and end frames
+ Returns True if the sequence is within the range, False otherwise
+ """
+ s_start = sequence.frame_final_start
+ s_end = sequence.frame_final_end
+ return start <= s_start <= end or start <= s_end <= end
+
+
+def set_preview_range(context, start, end):
+ """Sets the preview range and timeline render range"""
+ if not (start and end) and start != 0:
+ raise AttributeError("Missing start or end parameter")
+
+ scene = context.scene
+
+ scene.frame_start = start
+ scene.frame_end = end
+ scene.frame_preview_start = start
+ scene.frame_preview_end = end
+
+
+def slice_selection(context, sequences):
+ """
+ Takes a list of sequences and breaks it down
+ into multiple lists of connected sequences
+
+ Returns a list of lists of sequences,
+ each list corresponding to a block of sequences
+ that are connected in time and sorted by frame_final_start
+ """
+ # Find when 2 sequences are not connected in time
+ if not sequences:
+ return []
+
+ break_ids = [0]
+ sorted_sequences = sorted(sequences, key=attrgetter("frame_final_start"))
+ last_sequence = sorted_sequences[0]
+ last_biggest_frame_end = last_sequence.frame_final_end
+ index = 0
+ for s in sorted_sequences:
+ if s.frame_final_start > last_biggest_frame_end + 1:
+ break_ids.append(index)
+ last_biggest_frame_end = max(last_biggest_frame_end, s.frame_final_end)
+ last_sequence = s
+ index += 1
+
+ # Create lists
+ break_ids.append(len(sorted_sequences))
+ cuts_count = len(break_ids) - 1
+ broken_selection = []
+ index = 0
+ while index < cuts_count:
+ temp_list = []
+ index_range = range(break_ids[index], break_ids[index + 1] - 1)
+ if len(index_range) == 0:
+ temp_list.append(sorted_sequences[break_ids[index]])
+ else:
+ for counter in range(break_ids[index], break_ids[index + 1]):
+ temp_list.append(sorted_sequences[counter])
+ if temp_list:
+ broken_selection.append(temp_list)
+ index += 1
+ return broken_selection
+
+
+def trim_strips(
+ context, start_frame, end_frame, select_mode, strips_to_trim=[], strips_to_delete=[]
+):
+ """
+ Remove the footage and audio between start_frame and end_frame.
+ """
+ trim_start = min(start_frame, end_frame)
+ trim_end = max(start_frame, end_frame)
+
+ strips_to_trim = [s for s in strips_to_trim if s.type in SequenceTypes.CUTABLE]
+
+ for s in strips_to_trim:
+ if s.frame_final_start < trim_start and s.frame_final_end > trim_end:
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ s.select = True
+ bpy.ops.sequencer.cut(frame=trim_start, type="SOFT", side="RIGHT")
+ bpy.ops.sequencer.cut(frame=trim_end, type="SOFT", side="LEFT")
+ strips_to_delete.append(context.selected_sequences[0])
+ continue
+ elif s.frame_final_start < trim_end and s.frame_final_end > trim_end:
+ s.frame_final_start = trim_end
+ elif s.frame_final_end > trim_start and s.frame_final_start < trim_start:
+ s.frame_final_end = trim_start
+
+ if strips_to_delete != []:
+ bpy.ops.sequencer.select_all(action="DESELECT")
+ for s in strips_to_delete:
+ s.select = True
+ bpy.ops.sequencer.delete()
+ return {"FINISHED"}
+
+
+def find_closest_surrounding_cuts(context, frame):
+ """
+ Returns a tuple of (strip_before, strip_after), the two closest sequences around a gap.
+ If the frame is in the middle of a strip, both strips may be the same.
+ """
+ strip_before = max(
+ context.sequences,
+ key=lambda s: s.frame_final_end
+ if s.frame_final_end <= frame
+ else s.frame_final_start
+ if s.frame_final_start <= frame
+ else 0,
+ )
+ strip_after = min(
+ context.sequences,
+ key=lambda s: s.frame_final_start
+ if s.frame_final_start >= frame
+ else s.frame_final_end
+ if s.frame_final_end >= frame
+ else 1000000,
+ )
+ return strip_before, strip_after
+
+
+def find_closest_surrounding_cuts_frames(context, frame):
+ before, after = find_closest_surrounding_cuts(context, frame)
+ if before == after:
+ frame_left, frame_right = before.frame_final_start, before.frame_final_end
+ else:
+ frame_left, frame_right = before.frame_final_end, after.frame_final_start
+ return frame_left, frame_right
+
+
+def get_sequences_under_cursor(context):
+ frame = context.scene.frame_current
+ under_cursor = [
+ s
+ for s in context.sequences
+ if s.frame_final_start <= frame and s.frame_final_end >= frame and not s.lock
+ ]
+ return under_cursor
+
+
+def sequencer_workaround_2_80_audio_bug(context):
+ for s in context.sequences:
+ if s.lock:
+ continue
+ s.select = True
+ bpy.ops.transform.seq_slide(value=(0, 0))
+ s.select = False
+ break
diff --git a/power_sequencer/operators/utils/global_settings.py b/power_sequencer/operators/utils/global_settings.py
new file mode 100644
index 00000000..88590783
--- /dev/null
+++ b/power_sequencer/operators/utils/global_settings.py
@@ -0,0 +1,110 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+class ProjectSettings:
+ RESOLUTION_X = 1920
+ RESOLUTION_Y = 1080
+ PROXY_RESOLUTION_X = 640
+ PROXY_RESOLUTION_Y = 360
+ PROXY_STRING = "_proxy"
+
+ class FOLDER_NAMES:
+ AUDIO = "audio"
+ IMG = "img"
+ VIDEO = "video"
+ IMG_ASSETS = "-assets"
+
+ def __dir__(self):
+ return self.FOLDER_NAMES.AUDIO, self.FOLDER_NAMES.IMG, self.FOLDER_NAMES.VIDEO
+
+
+class SequenceTypes:
+ """
+ Tuples of identifiers to check if a strip is of a certain type or type group
+ """
+
+ VIDEO = ("MOVIE", "MOVIECLIP", "META", "SCENE")
+ EFFECT = (
+ "CROSS",
+ "ADD",
+ "SUBTRACT",
+ "ALPHA_OVER",
+ "ALPHA_UNDER",
+ "GAMMA_CROSS",
+ "MULTIPLY",
+ "OVER_DROP",
+ "WIPE",
+ "GLOW",
+ "TRANSFORM",
+ "COLOR",
+ "SPEED",
+ "ADJUSTMENT",
+ "GAUSSIAN_BLUR",
+ )
+ TRANSITION = ("CROSS", "GAMMA_CROSS", "WIPE")
+ SOUND = ("SOUND",)
+ IMAGE = ("IMAGE",)
+ TRANSITIONABLE = (
+ VIDEO + IMAGE + ("MULTICAM", "GAUSSIAN_BLUR", "TRANSFORM", "ADJUSTMENT", "SPEED")
+ )
+ # Strips that can be cut. If most effect strips are linked to their inputs
+ # and shouldn't be cut, some can be edited directly
+ CUTABLE = VIDEO + SOUND + IMAGE + ("MULTICAM", "COLOR", "ADJUSTMENT")
+
+
+EXTENSIONS_IMG = (
+ "jpeg",
+ "jpg",
+ "png",
+ "tga",
+ "tiff",
+ "tif",
+ "exr",
+ "hdr",
+ "bmp",
+ "cin",
+ "dpx",
+ "psd",
+)
+EXTENSIONS_AUDIO = (".wav", ".mp3", ".ogg", ".flac", ".opus")
+EXTENSIONS_VIDEO = (
+ ".mp4",
+ ".avi",
+ ".mts",
+ ".flv",
+ ".mkv",
+ ".mov",
+ ".mpg",
+ ".mpeg",
+ ".vob",
+ ".ogv",
+ "webm",
+)
+EXTENSIONS_ALL = tuple(list(EXTENSIONS_IMG) + list(EXTENSIONS_AUDIO) + list(EXTENSIONS_VIDEO))
+
+
+class Extensions:
+ """
+ Tuples of file types for checks when importing files
+ """
+
+ DICT = {"img": EXTENSIONS_IMG, "audio": EXTENSIONS_AUDIO, "video": EXTENSIONS_VIDEO}
+
+
+class SearchMode:
+ NEXT = 1
+ CHANNEL = 2
+ ALL = 3
diff --git a/power_sequencer/operators/utils/info_progress_bar.py b/power_sequencer/operators/utils/info_progress_bar.py
new file mode 100644
index 00000000..a3049bf5
--- /dev/null
+++ b/power_sequencer/operators/utils/info_progress_bar.py
@@ -0,0 +1,72 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import time
+
+
+class InfoProgressBar:
+ """
+ Draws a progress bar in the info header area
+ """
+
+ def __init__(self, progress_min=0, progress_max=100):
+ assert progress_min < progress_max
+
+ self.progress_min = progress_min
+ self.progress_max = progress_max
+
+ self._progress = bpy.props.FloatProperty(
+ default=self.progress_min,
+ min=self.progress_min,
+ max=self.progress_max,
+ subtype="PERCENTAGE",
+ )
+ self._visible = False
+
+ def update(self, context):
+ for area in context.screen.areas:
+ if area.type == "INFO":
+ area.tag_redraw()
+
+ def draw(self):
+ if self.progress >= self.progress_max:
+ self.visible = False
+ else:
+ self.layout.prop(self, "_progress", text="Progress", slider=True)
+
+ @property
+ def progress(self):
+ return self._progress
+
+ @progress.setter
+ def progress(self, value):
+ self._progress = min(max(self.progress_min, value), self.progress_max)
+
+ @property
+ def visible(self):
+ return self._visible
+
+ @visible.setter
+ def visible(self, value):
+ self._visible = value
+
+ if self._visible:
+ bpy.types.INFO_HT_header.append(self.draw)
+ bpy.app.handlers.scene_update_post.add(self.update)
+ else:
+ bpy.types.INFO_HT_header.remove(self.draw)
+ bpy.app.handlers.scene_update_post.remove(self.update)
diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/__init__.py b/power_sequencer/scripts/BPSProxy/bpsproxy/__init__.py
new file mode 100644
index 00000000..f14cfb6a
--- /dev/null
+++ b/power_sequencer/scripts/BPSProxy/bpsproxy/__init__.py
@@ -0,0 +1,16 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/__main__.py b/power_sequencer/scripts/BPSProxy/bpsproxy/__main__.py
new file mode 100644
index 00000000..d8f25204
--- /dev/null
+++ b/power_sequencer/scripts/BPSProxy/bpsproxy/__main__.py
@@ -0,0 +1,171 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Tool to render video proxies using FFMPEG
+Offers mp4 and webm options
+"""
+import argparse as ap
+import glob as g
+import logging as lg
+import os.path as osp
+import sys
+from itertools import compress, starmap, tee
+
+from .call import call, call_makedirs
+from .commands import get_commands, get_commands_vi
+from .config import CONFIG as C
+from .config import LOGGER, LOGLEV
+from .utils import checktools, printw, printd, prints, ToolError
+
+
+def find_files(
+ directory=".", ignored_directory=C["proxy_directory"], extensions=C["extensions"]["all"]
+):
+ """
+ Find files to process.
+
+ Parameters
+ ----------
+ directory: str
+ Working directory.
+ ignored_directory: str
+ Don't check for files in this directory. By default `BL_proxy`.
+ extensions: set(str)
+ Set of file extensions for filtering the directory tree.
+
+ Returns
+ -------
+ out: list(str)
+ List of file paths to be processed.
+ """
+ if not osp.isdir(directory):
+ raise ValueError(("The given path '{}' is not a valid directory.".format(directory)))
+ xs = g.iglob("{}/**".format(osp.abspath(directory)), recursive=True)
+ xs = filter(lambda x: osp.isfile(x), xs)
+ xs = filter(lambda x: ignored_directory not in osp.dirname(x), xs)
+ xs = [x for x in xs if osp.splitext(x)[1].lower() in extensions]
+ return xs
+
+
+def parse_arguments(cfg):
+ """
+ Uses `argparse` to parse the command line arguments.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+
+ Returns
+ -------
+ out: Namespace
+ Command line arguments.
+ """
+ p = ap.ArgumentParser(description="Create proxies for Blender VSE using FFMPEG.")
+ p.add_argument(
+ "working_directory",
+ nargs="?",
+ default=".",
+ help="The directory containing media to create proxies for",
+ )
+ p.add_argument(
+ "-p",
+ "--preset",
+ default="mp4",
+ choices=cfg["presets"],
+ help="a preset name for proxy encoding",
+ )
+ p.add_argument(
+ "-s",
+ "--sizes",
+ nargs="+",
+ type=int,
+ default=[25],
+ choices=cfg["proxy_sizes"],
+ help="A list of sizes of the proxies to render, either 25, 50, or 100",
+ )
+ p.add_argument(
+ "-v", "--verbose", action="count", default=0, help="Increase verbosity level (eg. -vvv)."
+ )
+ p.add_argument(
+ "--dry-run",
+ action="store_true",
+ help=(
+ "Run the script without actual rendering or creating files and"
+ " folders. For DEBUGGING purposes"
+ ),
+ )
+
+ clargs = p.parse_args()
+ # normalize directory
+ clargs.working_directory = osp.abspath(clargs.working_directory)
+ # --dry-run implies maximum verbosity level
+ clargs.verbose = 99999 if clargs.dry_run else clargs.verbose
+ return clargs
+
+
+def main():
+ """
+ Script entry point.
+ """
+ tools = ["ffmpeg", "ffprobe"]
+ try:
+ # get command line arguments and set log level
+ clargs = parse_arguments(C)
+ lg.basicConfig(level=LOGLEV[min(clargs.verbose, len(LOGLEV) - 1)])
+
+ # log basic command line arguments
+ clargs.dry_run and LOGGER.info("DRY-RUN")
+ LOGGER.info("WORKING-DIRECTORY :: {}".format(clargs.working_directory))
+ LOGGER.info("PRESET :: {}".format(clargs.preset))
+ LOGGER.info("SIZES :: {}".format(clargs.sizes))
+
+ # check for external dependencies
+ checktools(tools)
+
+ # find files to process
+ path_i = find_files(clargs.working_directory)
+ kwargs = {"path_i": path_i}
+
+ printw(C, "Creating directories if necessary")
+ call_makedirs(C, clargs, **kwargs)
+
+ printw(C, "Checking for existing proxies")
+ cmds = tee(get_commands(C, clargs, what="check", **kwargs))
+ stdouts = call(C, clargs, cmds=cmds[0], check=False, shell=True, **kwargs)
+ checks = map(lambda s: s.strip().split(), stdouts)
+ checks = starmap(lambda fst, *tail: not all(fst == t for t in tail), checks)
+ kwargs["path_i"] = list(compress(kwargs["path_i"], checks))
+
+ if len(kwargs["path_i"]) != 0:
+ printw(C, "Processing", s="\n")
+ cmds = get_commands_vi(C, clargs, **kwargs)
+ call(C, clargs, cmds=cmds, **kwargs)
+ else:
+ printd(C, "All proxies exist or no files found, nothing to process", s="\n")
+ printd(C, "Done")
+ except (ToolError, ValueError) as e:
+ LOGGER.error(e)
+ prints(C, "Exiting")
+ except KeyboardInterrupt:
+ prints(C, "DirtyInterrupt. Exiting", s="\n\n")
+ sys.exit()
+
+
+# this is so it can be ran as a module: `python3 -m bpsrender` (for testing)
+if __name__ == "__main__":
+ main()
diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/call.py b/power_sequencer/scripts/BPSProxy/bpsproxy/call.py
new file mode 100644
index 00000000..dd74e3a8
--- /dev/null
+++ b/power_sequencer/scripts/BPSProxy/bpsproxy/call.py
@@ -0,0 +1,95 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+# import multiprocessing as mp
+import os
+import subprocess as sp
+import sys
+
+from functools import partial
+from itertools import chain, tee
+from tqdm import tqdm
+from .config import LOGGER
+from .utils import get_dir, kickstart
+
+WINDOWS = ("win32", "cygwin")
+
+
+def call_makedirs(cfg, clargs, **kwargs):
+ """
+ Make BL_proxy directories if necessary.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments.
+ kwargs: dict
+ MANDATORY: path_i
+ Dictionary with additional information from previous step.
+ """
+ path_i = kwargs["path_i"]
+ path_d = map(partial(get_dir, cfg, clargs, **kwargs), path_i)
+ path_d = tee(chain(*path_d))
+ kickstart(map(lambda p: LOGGER.info("Directory @ {}".format(p)), path_d[0]))
+ if clargs.dry_run:
+ return
+ path_d = (os.makedirs(p, exist_ok=True) for p in path_d[1])
+ kickstart(path_d)
+
+
+def call(cfg, clargs, *, cmds, **kwargs):
+ """
+ Generic subprocess calls.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments.
+ cmds: iter(tuple(str))
+ kwargs: dict
+ MANDATORY: path_i
+ Dictionary with additional information from previous step.
+
+ Returns
+ -------
+ out: str
+ Stdout & Stderr gathered from subprocess call.
+ """
+ kwargs_s = {
+ "stdout": sp.PIPE,
+ "stderr": sp.STDOUT,
+ "universal_newlines": True,
+ "check": kwargs.get("check", True),
+ "shell": kwargs.get("shell", False),
+ "creationflags": sp.CREATE_NEW_PROCESS_GROUP if sys.platform in WINDOWS else 0,
+ }
+ if kwargs_s["shell"]:
+ cmds = map(lambda cmd: (cmd[0], " ".join(cmd[1])), cmds)
+ cmds = tee(cmds)
+ kickstart(map(lambda cmd: LOGGER.debug("CALL :: {}".format(cmd[1])), cmds[0]))
+ if clargs.dry_run:
+ return []
+ n = len(kwargs["path_i"])
+ ps = tqdm(
+ map(lambda cmd: sp.run(cmd[1], **kwargs_s), cmds[1]),
+ total=n,
+ unit="file" if n == 1 else "files",
+ )
+ return [p.stdout for p in ps]
diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/commands.py b/power_sequencer/scripts/BPSProxy/bpsproxy/commands.py
new file mode 100644
index 00000000..d481c58f
--- /dev/null
+++ b/power_sequencer/scripts/BPSProxy/bpsproxy/commands.py
@@ -0,0 +1,190 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import os.path as osp
+import shlex as sl
+from itertools import chain
+from .utils import get_path
+
+
+def get_commands_check(cfg, clargs, **kwargs):
+ """
+ ffprobe subprocess command generation.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments.
+ cmds: iter(tuple(str))
+ kwargs: dict
+ MANDATORY: path_i_1, path_o_1
+ Dictionary with additional information from previous step.
+
+ Returns
+ -------
+ out: iter(tuple(str))
+ Iterator containing commands.
+ """
+ cmd = (
+ "ffprobe -v error -select_streams v:0 -show_entries stream=nb_frames -of"
+ " default=noprint_wrappers=1:nokey=1 '{file}'"
+ )
+ out = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes)
+ out = map(lambda f: cmd.format(file=f), out)
+ out = sl.split(cmd.format(file=kwargs["path_i_1"]) + " && " + " && ".join(out))
+ return iter((out,))
+
+
+def get_commands_image_1(cfg, clargs, **kwargs):
+ """
+ ffmpeg subprocess command generation for processing an image.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments.
+ cmds: iter(tuple(str))
+ kwargs: dict
+ MANDATORY: path_i_1, path_o_1
+ Dictionary with additional information from previous step.
+
+ Returns
+ -------
+ out: iter(tuple(str))
+ Iterator containing commands.
+ """
+ cmd = "ffmpeg -y -v quiet -stats -i '{path_i_1}' {common_all}"
+ common = "-f apng -filter:v scale=iw*{size}:ih*{size} '{path_o_1}'"
+ common_all = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes)
+ common_all = map(
+ lambda s: common.format(size=s[0] / 100.0, path_o_1=s[1]), zip(clargs.sizes, common_all)
+ )
+ common_all = " ".join(common_all)
+ out = sl.split(cmd.format(path_i_1=kwargs["path_i_1"], common_all=common_all))
+ return iter((out,))
+
+
+def get_commands_video_1(cfg, clargs, **kwargs):
+ """
+ ffmpeg subprocess command generation for processing a video.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments.
+ cmds: iter(tuple(str))
+ kwargs: dict
+ MANDATORY: path_i_1, path_o_1
+ Dictionary with additional information from previous step.
+
+ Returns
+ -------
+ out: iter(tuple(str))
+ Iterator containing commands.
+ """
+ cmd = "ffmpeg -y -v quiet -stats -i '{path_i_1}' {common_all}"
+ common = (
+ "-pix_fmt yuv420p"
+ " -g 1"
+ " -sn -an"
+ " -vf colormatrix=bt601:bt709"
+ " -vf scale=ceil(iw*{size}/2)*2:ceil(ih*{size}/2)*2"
+ " {preset}"
+ " '{path_o_1}'"
+ )
+ common_all = map(lambda s: kwargs["path_o_1"].format(size=s), clargs.sizes)
+ common_all = map(
+ lambda s: common.format(
+ preset=cfg["presets"][clargs.preset], size=s[0] / 100.0, path_o_1=s[1]
+ ),
+ zip(clargs.sizes, common_all),
+ )
+ common_all = " ".join(common_all)
+ out = sl.split(cmd.format(path_i_1=kwargs["path_i_1"], common_all=common_all))
+ return iter((out,))
+
+
+def get_commands(cfg, clargs, *, what, **kwargs):
+ """
+ Delegates the creation of commands lists to appropriate functions based on `what` parameter.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments.
+ cmds: iter(tuple(str))
+ what: str
+ Determines the returned value (see: Returns[out]).
+ kwargs: dict
+ MANDATORY: path_i
+ Dictionary with additional information from previous step.
+
+ Returns
+ -------
+ out: iter(tuple(str, tuple(str)))
+ An iterator with the 1st element as a tag (the `what` parameter) and the 2nd
+ element as the iterator of the actual commands.
+ """
+ get_commands_f = {
+ "video": get_commands_video_1,
+ "image": get_commands_image_1,
+ "check": get_commands_check,
+ }
+ ps = (
+ kwargs["path_i"]
+ if what not in cfg["extensions"]
+ else filter(
+ lambda p: osp.splitext(p)[1].lower() in cfg["extensions"][what], kwargs["path_i"]
+ )
+ )
+ ps = map(lambda p: (p, get_path(cfg, clargs, p, **kwargs)), ps)
+ out = chain.from_iterable(
+ map(lambda p: get_commands_f[what](cfg, clargs, path_i_1=p[0], path_o_1=p[1], **kwargs), ps)
+ )
+ return map(lambda c: (what, c), out)
+
+
+def get_commands_vi(cfg, clargs, **kwargs):
+ """
+ Delegates the creation of commands lists to appropriate functions for video/image processing.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments.
+ cmds: iter(tuple(str))
+ kwargs: dict
+ MANDATORY: path_i_1, path_o_1
+ Dictionary with additional information from previous step.
+
+ Returns
+ -------
+ out: iter(tuple(str, tuple(str)))
+ An iterator with the 1st element as a tag (the `what` parameter) and the 2nd
+ element as the iterator of the actual commands.
+ """
+ ws = filter(lambda x: x is not "all", cfg["extensions"])
+ return chain.from_iterable(map(lambda w: get_commands(cfg, clargs, what=w, **kwargs), ws))
diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/config.py b/power_sequencer/scripts/BPSProxy/bpsproxy/config.py
new file mode 100644
index 00000000..eada47d5
--- /dev/null
+++ b/power_sequencer/scripts/BPSProxy/bpsproxy/config.py
@@ -0,0 +1,41 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import multiprocessing as mp
+from itertools import chain
+import logging as lg
+
+
+CONFIG = {
+ "logger": "BPS",
+ "proxy_directory": "BL_proxy",
+ "proxy_sizes": (25, 50, 100),
+ "extensions": {
+ "video": {".mp4", ".mkv", ".mov", ".flv", ".mts"},
+ "image": {".png", ".jpg", ".jpeg"},
+ },
+ "presets": {
+ "webm": "-c:v libvpx -crf 25 -speed 16 -threads {}".format(str(mp.cpu_count())),
+ "mp4": "-c:v libx264 -crf 25 -preset faster",
+ "nvenc": "-c:v h264_nvenc -qp 25 -preset fast",
+ },
+ "pre": {"work": "»", "done": "•", "skip": "~"},
+}
+CONFIG["extensions"]["all"] = set(chain(*CONFIG["extensions"].values()))
+
+LOGGER = lg.getLogger(CONFIG["logger"])
+LOGLEV = [lg.INFO, lg.DEBUG]
+LOGLEV = [None] + sorted(LOGLEV, reverse=True)
diff --git a/power_sequencer/scripts/BPSProxy/bpsproxy/utils.py b/power_sequencer/scripts/BPSProxy/bpsproxy/utils.py
new file mode 100644
index 00000000..832a0beb
--- /dev/null
+++ b/power_sequencer/scripts/BPSProxy/bpsproxy/utils.py
@@ -0,0 +1,109 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Collection of utility functions, class-independent
+"""
+import os.path as osp
+from collections import deque
+from shutil import which
+
+
+class ToolError(Exception):
+ """Raised if external dependencies aren't found on system.
+ """
+
+ pass
+
+
+def checktools(tools):
+ tools = [(t, which(t) or "") for t in tools]
+ check = {"tools": tools, "test": all(map(lambda x: x[1], tools))}
+ if not check["test"]:
+ msg = ["BPSProxy couldn't find external dependencies:"]
+ msg += [
+ "[{check}] {tool}: {path}".format(
+ check="v" if path is not "" else "X", tool=tool, path=path or "NOT FOUND"
+ )
+ for tool, path in check["tools"]
+ ]
+ msg += [
+ (
+ "Check if you have them properly installed and available in the PATH"
+ " environemnt variable."
+ )
+ ]
+ raise ToolError("\n".join(msg))
+
+
+def get_path_video(cfg, clargs, path, **kwargs):
+ return osp.join(
+ osp.dirname(path), cfg["proxy_directory"], osp.basename(path), "proxy_{size}.avi"
+ )
+
+
+def get_path_image(cfg, clargs, path, **kwargs):
+ return osp.join(
+ osp.dirname(path),
+ cfg["proxy_directory"],
+ "images",
+ "{size}",
+ "{file}_proxy.jpg".format(file=osp.basename(path)),
+ )
+
+
+def get_path(cfg, clargs, path, **kwargs):
+ get_path_f = {"video": get_path_video, "image": get_path_image}
+ what = what_vi(cfg, clargs, path, **kwargs)
+ return get_path_f[what](cfg, clargs, path, **kwargs)
+
+
+def get_dir_video(cfg, clargs, path, **kwargs):
+ return iter((osp.join(osp.dirname(path), cfg["proxy_directory"], osp.basename(path)),))
+
+
+def get_dir_image(cfg, clargs, path, **kwargs):
+ ps = osp.join(osp.dirname(path), cfg["proxy_directory"], "images", "{size}")
+ return map(lambda s: ps.format(size=s), clargs.sizes)
+
+
+def get_dir(cfg, clargs, path, **kwargs):
+ get_dir_f = {"video": get_dir_video, "image": get_dir_image}
+ what = what_vi(cfg, clargs, path, **kwargs)
+ return get_dir_f[what](cfg, clargs, path, **kwargs)
+
+
+def what_vi(cfg, clargs, p, **kwargs):
+ return "video" if osp.splitext(p)[1].lower() in cfg["extensions"]["video"] else "image"
+
+
+def kickstart(it):
+ deque(it, maxlen=0)
+
+
+def printw(cfg, text, s="\n", e="...", p="", **kwargs):
+ p = p or cfg["pre"]["work"]
+ print("{s}{p} {}{e}".format(text, s=s, e=e, p=p), **kwargs)
+
+
+def printd(cfg, text, s="", e=".", p="", **kwargs):
+ p = p or cfg["pre"]["done"]
+ printw(cfg, text, s=s, e=e, p=p, **kwargs)
+
+
+def prints(cfg, text, s="", e=".", p="", **kwargs):
+ p = p or cfg["pre"]["skip"]
+ printw(cfg, text, s=s, e=e, p=p, **kwargs)
diff --git a/power_sequencer/scripts/BPSProxy/setup.py b/power_sequencer/scripts/BPSProxy/setup.py
new file mode 100644
index 00000000..22aacf60
--- /dev/null
+++ b/power_sequencer/scripts/BPSProxy/setup.py
@@ -0,0 +1,55 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+from setuptools import setup
+
+
+def readme():
+ with open("README.rst") as f:
+ return f.read()
+
+
+setup(
+ name="bpsproxy",
+ version="0.1.3.post1",
+ description="Blender Power Sequencer proxy generator tool",
+ long_description=readme(),
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: End Users/Desktop",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+ "Natural Language :: English",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3",
+ "Topic :: Multimedia :: Video",
+ "Topic :: Utilities",
+ ],
+ url="https://gitlab.com/razcore/bpsproxy",
+ keywords="blender proxy vse sequence editor productivity",
+ author="Răzvan C. Rădulescu",
+ author_email="razcore.art@gmail.com",
+ license="GPLv3",
+ packages=["bpsproxy"],
+ install_requires=["tqdm"],
+ zip_safe=False,
+ entry_points={"console_scripts": ["bpsproxy=bpsproxy.__main__:main"]},
+ include_package_data=True,
+)
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/__init__.py b/power_sequencer/scripts/BPSRender/bpsrender/__init__.py
new file mode 100644
index 00000000..35a40273
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/__init__.py
@@ -0,0 +1,17 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/__main__.py b/power_sequencer/scripts/BPSRender/bpsrender/__main__.py
new file mode 100644
index 00000000..07c84fe2
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/__main__.py
@@ -0,0 +1,147 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+"""
+Renders videos edited in Blender 3D's Video Sequence Editor using multiple CPU
+cores. Original script by Justin Warren:
+https://github.com/sciactive/pulverize/blob/master/pulverize.py
+Modified by sudopluto (Pranav Sharma), gdquest (Nathan Lovato) and
+razcore (Razvan Radulescu)
+
+Under GPLv3 license
+"""
+import argparse as ap
+import os.path as osp
+import sys
+from functools import partial
+
+from .calls import call
+from .config import CONFIG as C
+from .config import LOGGER
+from .helpers import BSError, ToolError, checktools, kickstart, prints
+from .setup import setup
+
+# https://github.com/mikeycal/the-video-editors-render-script-for-blender#configuring-the-script
+# there seems no easy way to grab the ram usage in a mulitplatform way
+# without writing platform dependent code, or by using a python module
+
+# Most popluar config is 4 cores, 8 GB ram, this is the default for the script
+# https://store.steampowered.com/hwsurvey/
+
+
+def parse_arguments(cfg):
+ """
+ Uses `argparse` to parse the command line arguments.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+
+ Returns
+ -------
+ out: Namespace
+ Command line arguments (normalized).
+ """
+ p = ap.ArgumentParser(
+ description="Multi-process Blender VSE rendering - will attempt to"
+ " create a folder called `render` inside of the folder"
+ " containing `blendfile`. Insider `render` another folder called"
+ " `parts` will be created for storing temporary files. These files"
+ " will be joined together as the last step to produce the final"
+ " render which will be stored inside `render` and it will have the"
+ " same name as `blendfile`"
+ )
+ p.add_argument(
+ "-o",
+ "--output",
+ default=".",
+ help="Output folder (will contain a `bpsrender` temp folder for" "rendering parts).",
+ )
+ p.add_argument(
+ "-w",
+ "--workers",
+ type=int,
+ default=cfg["cpu_count"],
+ help="Number of workers in the pool (for video rendering).",
+ )
+ p.add_argument(
+ "-v", "--verbose", action="count", default=0, help="Increase verbosity level (eg. -vvv)."
+ )
+ p.add_argument(
+ "--dry-run",
+ action="store_true",
+ help=(
+ "Run the script without actual rendering or creating files and"
+ " folders. For DEBUGGING purposes"
+ ),
+ )
+ p.add_argument("-s", "--start", type=int, default=None, help="Start frame")
+ p.add_argument("-e", "--end", type=int, default=None, help="End frame")
+ p.add_argument(
+ "-m", "--mixdown-only", action="store_true", help="ONLY render the audio MIXDOWN"
+ )
+ p.add_argument(
+ "-c",
+ "--concatenate-only",
+ action="store_true",
+ help="ONLY CONCATENATE the (already) available video chunks",
+ )
+ p.add_argument(
+ "-d",
+ "--video-only",
+ action="store_true",
+ help="ONLY render the VIDEO (implies --concatenate-only).",
+ )
+ p.add_argument(
+ "-j",
+ "--join-only",
+ action="store_true",
+ help="ONLY JOIN the mixdown with the video. This will produce the" " final render",
+ )
+ p.add_argument("blendfile", help="Blender project file to render.")
+
+ clargs = p.parse_args()
+ clargs.blendfile = osp.abspath(clargs.blendfile)
+ clargs.output = osp.abspath(clargs.output)
+ # --video-only implies --concatenate-only
+ clargs.concatenate_only = clargs.concatenate_only or clargs.video_only
+ # --dry-run implies maximum verbosity level
+ clargs.verbose = 99999 if clargs.dry_run else clargs.verbose
+ return clargs
+
+
+def main():
+ """
+ Script entry point.
+ """
+ tools = ["blender", "ffmpeg"]
+ try:
+ clargs = parse_arguments(C)
+ checktools(tools)
+ cmds, kwargs = setup(C, clargs)
+ kickstart(map(partial(call, C, clargs, **kwargs), cmds))
+ except (BSError, ToolError) as e:
+ LOGGER.error(e)
+ except KeyboardInterrupt:
+ # TODO: add actual clean up code
+ prints(C, "DirtyInterrupt. Exiting", s="\n\n", e="...")
+ sys.exit()
+
+
+# this is so it can be ran as a module: `python3 -m bpsrender` (for testing)
+if __name__ == "__main__":
+ main()
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/mixdown.py b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/mixdown.py
new file mode 100644
index 00000000..a6b885cc
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/mixdown.py
@@ -0,0 +1,30 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import os.path as osp
+import sys
+
+
+for strip in bpy.context.scene.sequence_editor.sequences_all:
+ if strip.type == "META":
+ continue
+ if strip.type != "SOUND":
+ strip.mute = True
+
+path = sys.argv[-1]
+ext = osp.splitext(path)[1][1:].upper()
+bpy.ops.sound.mixdown(filepath=path, check_existing=False, container=ext, codec=ext)
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/probe.py b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/probe.py
new file mode 100644
index 00000000..92cffa60
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/probe.py
@@ -0,0 +1,30 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+EXT = {
+ "AVI_JPEG": ".avi",
+ "AVI_RAW": ".avi",
+ "FFMPEG": {"MKV": ".mkv", "OGG": ".ogv", "QUICKTIME": ".mov", "AVI": ".avi", "MPEG4": ".mp4"},
+}
+
+scene = bpy.context.scene
+
+ext = EXT.get(scene.render.image_settings.file_format, "UNDEFINED")
+if scene.render.image_settings.file_format == "FFMPEG":
+ ext = ext[scene.render.ffmpeg.format]
+print("\nBPS:{} {} {}\n".format(scene.frame_start, scene.frame_end, ext))
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/bscripts/video.py b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/video.py
new file mode 100644
index 00000000..590e73eb
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/bscripts/video.py
@@ -0,0 +1,19 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+bpy.context.scene.render.ffmpeg.audio_codec = "NONE"
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/calls.py b/power_sequencer/scripts/BPSRender/bpsrender/calls.py
new file mode 100644
index 00000000..5a223dd6
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/calls.py
@@ -0,0 +1,410 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+# IMPURE
+import multiprocessing as mp
+import os
+import signal as sig
+import subprocess as sp
+from functools import partial, reduce
+from itertools import chain, islice, starmap, tee
+from multiprocessing import Queue
+
+from tqdm import tqdm
+
+from .config import LOGGER
+from .helpers import BSError, checkblender, kickstart, printd, prints, printw
+
+
+def chunk_frames(cfg, clargs, cmds, **kwargs):
+ """
+ Recover the chunk start/end frames from the constructed commands for the
+ video step. This is necessary to preserve purity until later steps.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`.
+ kwargs: dict
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter(tuple)
+ Start/end pairs of frames corresponding to the chunk commands created at
+ the video step.
+ """
+ out = map(lambda x: (x, islice(x, 1, None)), cmds)
+ out = map(lambda x: zip(*x), out)
+ out = map(lambda x: filter(lambda y: y[0] in ("-s", "-e"), x), out)
+ out = map(lambda x: map(lambda y: int(y[1]), x), out)
+ out = map(lambda x: reduce(lambda acc, y: acc + (y,), x, ()), out)
+ return out
+
+
+def append_chunks_file(cfg, clargs, cmds, **kwargs):
+ """
+ IMPURE
+ Helper function for creating the chunks file that will be used by `ffmpeg`
+ to concatenate the chunks into one video file.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`.
+ kwargs: dict
+ MANDATORY w_frame_start, w_frame_end, ext
+ Dictionary with additional information from the setup step.
+ """
+ with open(kwargs["chunks_file_path"], "a") as f:
+ for fs, fe in chunk_frames(cfg, clargs, cmds, **kwargs):
+ f.write(
+ "file '{rcp}{fs}-{fe}{ext}'\n".format(
+ rcp=kwargs["render_chunk_path"].rstrip("#"),
+ fs="{fs:0{frame_pad}d}".format(fs=fs, **cfg),
+ fe="{fe:0{frame_pad}d}".format(fe=fe, **cfg),
+ **kwargs
+ )
+ )
+
+
+def call_probe(cfg, clargs, cmds, **kwargs):
+ """
+ IMPURE
+ Probe `clargs.blendfile` for frame start, frame end and extension (for
+ video only).
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`.
+ kwargs: dict
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: dict
+ Dictionary with info extracted from `clargs.blendfile`, namely: start
+ frame, end frame and extension (only useful for video step).
+ """
+ kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}
+
+ printw(cfg, "Probing")
+ printw(cfg, "Input(blend) @ {}".format(clargs.blendfile), s="")
+ frame_start, frame_end, ext = (0, 0, "")
+ if not clargs.dry_run:
+ with sp.Popen(next(cmds), **kwargs_p) as cp:
+ try:
+ tmp = map(partial(checkblender, "PROBE", [cfg["probe_py"]], cp), cp.stdout)
+ tmp = filter(lambda x: x.startswith("BPS"), tmp)
+ tmp = map(lambda x: x[4:].strip().split(), tmp)
+ frame_start, frame_end, ext = chain(*tmp)
+ except BSError as e:
+ LOGGER.error(e)
+ except KeyboardInterrupt:
+ raise
+ finally:
+ cp.terminate()
+ returncode = cp.poll()
+ if returncode != 0:
+ raise sp.CalledProcessError(returncode, cp.args)
+ frame_start = frame_start if clargs.start is None else clargs.start
+ frame_end = frame_end if clargs.end is None else clargs.end
+ out = {
+ "frame_start": int(frame_start),
+ "frame_end": int(frame_end),
+ "frames_total": int(frame_end) - int(frame_start) + 1,
+ "ext": ext,
+ }
+ if out["ext"] == "UNDEFINED":
+ raise BSError("Video extension is {ext}. Stopping!".format(ext=ext))
+ printd(cfg, "Probing done")
+ return out
+
+
+def call_mixdown(cfg, clargs, cmds, **kwargs):
+ """
+ IMPURE
+ Calls blender to render the audio mixdown.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`.
+ kwargs: dict
+ MANDATORY render_mixdown_path
+ Dictionary with additional information from the setup step.
+ """
+ kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}
+
+ printw(cfg, "Rendering mixdown")
+ printw(cfg, "Output @ {}".format(kwargs["render_mixdown_path"]), s="")
+ if not clargs.dry_run:
+ with sp.Popen(next(cmds), **kwargs_p) as cp:
+ try:
+ tmp = map(partial(checkblender, "MIXDOWN", [cfg["mixdown_py"]], cp), cp.stdout)
+ tmp = filter(lambda x: x.startswith("BPS"), tmp)
+ tmp = map(lambda x: x[4:].strip().split(), tmp)
+ kickstart(tmp)
+ except BSError as e:
+ LOGGER.error(e)
+ except KeyboardInterrupt:
+ raise
+ finally:
+ cp.terminate()
+ returncode = cp.poll()
+ if returncode != 0:
+ raise sp.CalledProcessError(returncode, cp.args)
+ printd(cfg, "Mixdown done")
+
+
+def call_chunk(cfg, clargs, queue, cmd, **kwargs):
+ """
+ IMPURE
+ Calls blender to render one chunk (which part is determined by `cmd`).
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmd: tuple
+ Tuple to be passed to `subprocess`.
+ kwargs: dict
+ Dictionary with additional information from the setup step.
+ """
+ sig.signal(sig.SIGINT, sig.SIG_IGN)
+ kwargs_p = {"stdout": sp.PIPE, "stderr": sp.STDOUT, "universal_newlines": True}
+
+ if not clargs.dry_run:
+ # can't use nice functional syntax if we want to simplify with `with`
+ with sp.Popen(cmd, **kwargs_p) as cp:
+ try:
+ tmp = map(
+ partial(
+ checkblender,
+ "VIDEO",
+ [cfg["video_py"], "The encoder timebase is not set"],
+ cp,
+ ),
+ cp.stdout,
+ )
+ tmp = filter(lambda x: x.startswith("Append frame"), tmp)
+ tmp = map(lambda x: x.split()[-1], tmp)
+ tmp = map(int, tmp)
+ tmp = map(lambda x: True, tmp)
+ kickstart(map(queue.put, tmp))
+ queue.put(False)
+ except BSError as e:
+ LOGGER.error(e)
+
+
+def call_video(cfg, clargs, cmds, **kwargs):
+ """
+ IMPURE
+ Multi-process call to blender for rendering the (video) chunks.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`.
+ kwargs: dict
+ Dictionary with additional information from the setup step.
+ """
+ printw(cfg, "Rendering video (w/o audio)")
+ printw(cfg, "Output @ {}".format(kwargs["render_chunk_path"]), s="")
+ try:
+ not clargs.dry_run and os.remove(kwargs["chunks_file_path"])
+ LOGGER.info("CALL-VIDEO: generating {}".format(kwargs["chunks_file_path"]))
+ except OSError as e:
+ LOGGER.info("CALL-VIDEO: skipping {}: {}".format(e.filename, e.strerror))
+
+ cmds, cmds_cf = tee(cmds)
+ (not clargs.dry_run and append_chunks_file(cfg, clargs, cmds_cf, **kwargs))
+ # prepare queue/worker
+ queues = queues_close = (Queue(),) * clargs.workers
+ # prpare processes
+ proc = starmap(
+ lambda q, cmd: mp.Process(target=partial(call_chunk, cfg, clargs, **kwargs), args=(q, cmd)),
+ zip(queues, cmds),
+ )
+ # split iterator in 2 for later joining the processes and sum
+ # one of them
+ proc, proc_close = tee(proc)
+ proc = map(lambda p: p.start(), proc)
+ try:
+ not clargs.dry_run and kickstart(proc)
+
+ # communicate with processes through the queues and use tqdm to show a
+ # simple terminal progress bar baesd on video total frames
+ queues = map(lambda q: iter(q.get, False), queues)
+ queues = chain(*queues)
+ queues = tqdm(queues, total=kwargs["frame_end"] - kwargs["frame_start"] + 1, unit="frames")
+ not clargs.dry_run and kickstart(queues)
+ except KeyboardInterrupt:
+ proc_close = map(lambda x: x.terminate(), proc_close)
+ not clargs.dry_run and kickstart(proc_close)
+ raise
+ finally:
+ # close and join processes and queues
+ proc_close = map(lambda x: x.join(), proc_close)
+ not clargs.dry_run and kickstart(proc_close)
+
+ queues_close = map(lambda q: (q, q.close()), queues_close)
+ queues_close = starmap(lambda q, _: q.join_thread(), queues_close)
+ not clargs.dry_run and kickstart(queues_close)
+ printd(cfg, "Video chunks rendering done")
+
+
+def call_concatenate(cfg, clargs, cmds, **kwargs):
+ """
+ IMPURE
+ Calls ffmpeg in order to concatenate the video chunks together.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`.
+ kwargs: dict
+ MANDATORY: render_video_path
+ Dictionary with additional information from the setup step.
+
+ Note
+ ----
+ It expects the video chunk files to already be available.
+ """
+ kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL}
+ printw(cfg, "Concatenating (video) chunks")
+ printw(cfg, "Output @ {}".format(kwargs["render_video_path"]), s="")
+ if not clargs.dry_run:
+ with sp.Popen(next(cmds), **kwargs_p) as cp:
+ try:
+ returncode = cp.wait()
+ if returncode != 0:
+ raise sp.CalledProcessError(returncode, cp.args)
+ except KeyboardInterrupt:
+ raise
+ finally:
+ cp.terminate()
+ printd(cfg, "Concatenating done")
+
+
+def call_join(cfg, clargs, cmds, **kwargs):
+ """
+ IMPURE
+ Calls ffmpeg for joining the audio mixdown and the video.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`.
+ kwargs: dict
+ MANDATORY: render_audiovideo_path
+ Dictionary with additional information from the setup step.
+
+ Note
+ ----
+ It expects the audio mixdown and video files to already be available.
+ """
+ kwargs_p = {"stdout": sp.DEVNULL, "stderr": sp.DEVNULL}
+ printw(cfg, "Joining audio/video")
+ printw(cfg, "Output @ {}".format(kwargs["render_audiovideo_path"]), s="")
+ if not clargs.dry_run:
+ with sp.Popen(next(cmds), **kwargs_p) as cp:
+ try:
+ returncode = cp.wait()
+ if returncode != 0:
+ raise sp.CalledProcessError(returncode, cp.args)
+ except KeyboardInterrupt:
+ raise
+ finally:
+ cp.terminate()
+ printd(cfg, "Joining done")
+
+
+def call(cfg, clargs, cmds, **kwargs):
+ """
+ IMPURE
+ Delegates work to appropriate `call_*` functions.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ cmds: iter(tuple)
+ Iterator of commands to be passed to `subprocess`
+ kwargs: dict
+ MANDATORY: render_audiovideo_path
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: dict or None
+ It passes on the output from the `call_*` functions. See `call_*` for
+ specific details.
+
+ Note
+ ----
+ It tries to be smart and skip steps if child subprocesses give errors.
+ Example if `--join-only` is passed, but the audio mixdown or video file
+ aren't available on hard drive.
+ """
+ calls = {
+ "probe": call_probe,
+ "mixdown": call_mixdown,
+ "video": call_video,
+ "concatenate": call_concatenate,
+ "join": call_join,
+ }
+ try:
+ out = calls[cmds[0]](cfg, clargs, cmds[1], **kwargs)
+ return out
+ except sp.CalledProcessError:
+ prints(
+ cfg,
+ ("WARNING:{}: Something went wrong when calling" " command - SKIPPING").format(cmds[0]),
+ )
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/commands.py b/power_sequencer/scripts/BPSRender/bpsrender/commands.py
new file mode 100644
index 00000000..dc669806
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/commands.py
@@ -0,0 +1,341 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import math as m
+from collections import OrderedDict
+from itertools import chain, islice
+
+from .config import LOGGER
+
+
+def get_commands_probe(cfg, clargs, **kwargs):
+ """
+ Create the command for probing the `clargs.blendfile`.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter(list)
+ An iterator for which each element is a list to be sent to functions like
+ `subprocess.run`.
+ """
+ out = (
+ "blender",
+ "--background",
+ clargs.blendfile,
+ "--python",
+ kwargs["probe_py_normalized"],
+ "--disable-autoexec",
+ )
+ LOGGER.debug("CMD-PROBE: {cmd}".format(cmd=" ".join(out)))
+ return iter((out,))
+
+
+def get_commands_chunk(cfg, clargs, **kwargs):
+ """
+ Create the command for rendering a (video) chunk from `clargs.blendfile`.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY render_chunk_path, w_frame_start, w_frame_end
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter(list)
+ An iterator for which each element is a list to be sent to functions like
+ `subprocess.run`.
+ """
+ out = (
+ "blender",
+ "--background",
+ clargs.blendfile,
+ "--python",
+ kwargs["video_py_normalized"],
+ "--disable-autoexec",
+ "--render-output",
+ kwargs["render_chunk_path"],
+ "-s",
+ str(kwargs["w_frame_start"]),
+ "-e",
+ str(kwargs["w_frame_end"]),
+ "--render-anim",
+ )
+ LOGGER.debug(
+ "CMD-CHUNK({w_frame_start}-{w_frame_end}): {cmd}".format(cmd=" ".join(out), **kwargs)
+ )
+ return iter((out,))
+
+
+def get_commands_video(cfg, clargs, **kwargs):
+ """
+ Create the list of commands (one command per chunk) for rendering a video
+ from `clargs.blendfile`.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY chunk_file_path, frame_start, frame_end, frames_total
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter(tuple)
+ An iterator for which each element is a tuple to be sent to functions like
+ `subprocess.run`.
+ """
+ LOGGER.debug("CMD-VIDEO:")
+ chunk_length = int(m.floor(kwargs["frames_total"] / clargs.workers))
+ out = map(lambda w: (w, kwargs["frame_start"] + w * chunk_length), range(clargs.workers))
+ out = map(
+ lambda x: (
+ x[1],
+ x[1] + chunk_length - 1 if x[0] != clargs.workers - 1 else kwargs["frame_end"],
+ ),
+ out,
+ )
+ out = map(
+ lambda x: get_commands(
+ cfg, clargs, "chunk", w_frame_start=x[0], w_frame_end=x[1], **kwargs
+ ),
+ out,
+ )
+ out = map(lambda x: x[1], out)
+ out = chain(*out)
+ return tuple(out)
+
+
+def get_commands_mixdown(cfg, clargs, **kwargs):
+ """
+ Create the command to render the mixdown from `clargs.blendfile`.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY render_mixdown_path
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter(tuple)
+ An iterator for which each element is a tuple to be sent to functions like
+ `subprocess.run`.
+ """
+ out = (
+ "blender --background {blendfile} --python {mixdown_py_normalized}"
+ " --disable-autoexec -- {render_mixdown_path}".format(**cfg, **vars(clargs), **kwargs)
+ )
+ out = (
+ "blender",
+ "--background",
+ clargs.blendfile,
+ "--python",
+ kwargs["mixdown_py_normalized"],
+ "--disable-autoexec",
+ "--",
+ kwargs["render_mixdown_path"],
+ )
+ LOGGER.debug("CMD-MIXDOWN: {cmd}".format(cmd=" ".join(out)))
+ return iter((out,))
+
+
+def get_commands_concatenate(cfg, clargs, **kwargs):
+ """
+ Create the command to concatenate the available video chunks generated
+ beforehand.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY chunks_file_path, render_video_path
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter(tuple)
+ An iterator for which each element is a tuple to be sent to functions like
+ `subprocess.run`.
+ """
+ out = (
+ "ffmpeg",
+ "-stats",
+ "-f",
+ "concat",
+ "-safe",
+ "-0",
+ "-i",
+ kwargs["chunks_file_path"],
+ "-c",
+ "copy",
+ "-y",
+ kwargs["render_video_path"],
+ )
+ LOGGER.debug("CMD-CONCATENATE: {cmd}".format(cmd=" ".join(out)))
+ return iter((out,))
+
+
+def get_commands_join(cfg, clargs, **kwargs):
+ """
+ Create the command to join the available audio mixdown and video generated
+ beforehand.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY chunks_file_path, render_video_path
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter(tuple)
+ An iterator for which each element is a tuple to be sent to functions like
+ `subprocess.run`.
+ """
+ out = (
+ "ffmpeg",
+ "-stats",
+ "-i",
+ kwargs["render_video_path"],
+ "-i",
+ kwargs["render_mixdown_path"],
+ "-map",
+ "0:v:0",
+ "-c:v",
+ "copy",
+ "-map",
+ "1:a:0",
+ "-c:a",
+ "aac",
+ "-b:a",
+ "192k",
+ "-y",
+ kwargs["render_audiovideo_path"],
+ )
+ LOGGER.debug("CMD-JOIN: {cmd}".format(cmd=" ".join(out)))
+ return iter((out,))
+
+
+def get_commands(cfg, clargs, what="", **kwargs):
+ """
+ Delegates the creation of commands lists to appropriate functions based on
+ `what` parameter.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ what: str (default = '')
+ Determines the returned value (see: Returns[out]).
+ kwargs: dict
+ MANDATORY -- see individual functions for the list of mandatory keys
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter or (str, iter)
+ |- what == '' is True
+ An iterator with elements of the type (str) for determining the order in
+ which to call the functions in the setup step.
+ NOTE: it skipps the "internal use only" functions.
+ |- else
+ A tuple with the 1st element as a tag (the `what` parameter) and the 2nd
+ element as the iterator of the actual commands.
+ """
+ get_commands_f = OrderedDict(
+ (
+ # internal use only
+ ("probe", get_commands_probe),
+ ("chunk", get_commands_chunk),
+ # direct connection to command line arguments - in order of execution
+ ("mixdown", get_commands_mixdown),
+ ("video", get_commands_video),
+ ("concatenate", get_commands_concatenate),
+ ("join", get_commands_join),
+ )
+ )
+
+ return (
+ islice(get_commands_f, 2, None)
+ if what == ""
+ else (what, get_commands_f[what](cfg, clargs, **kwargs))
+ )
+
+
+def get_commands_all(cfg, clargs, **kwargs):
+ """
+ Prepare the list of commands to be executed depending on the command line
+ arguments.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY -- see individual functions for the list of mandatory keys
+ Dictionary with additional information from the setup step.
+
+ Returns
+ -------
+ out: iter((str, tuple))
+ An iterator for which each element is a (str, iter(tuple)). The string
+ value is for tagging the iterator command list (2nd element) for filtering
+ later based on the given command line arguments.
+ """
+ end = "_only"
+ out = filter(lambda x: x[0].endswith(end), vars(clargs).items())
+ out = map(lambda x: (x[0][: -len(end)], x[1]), out)
+ order = list(get_commands(cfg, clargs))
+ out = sorted(out, key=lambda x: order.index(x[0]))
+ out = (
+ map(lambda k: k[0], out)
+ if all(map(lambda k: not k[1], out))
+ else map(lambda k: k[0], filter(lambda k: k[1], out))
+ )
+ out = map(lambda k: get_commands(cfg, clargs, k, **kwargs), out)
+ return out
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/config.py b/power_sequencer/scripts/BPSRender/bpsrender/config.py
new file mode 100644
index 00000000..b87e3ea3
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/config.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import logging as lg
+import multiprocessing as mp
+import os.path as osp
+
+CONFIG = {
+ "logger": "BPS",
+ "cpu_count": min(int(mp.cpu_count() / 2), 6),
+ "bs_path": osp.join(osp.dirname(osp.abspath(__file__)), "bscripts"),
+ "frame_pad": 7,
+ "parts_folder": "bpsrender",
+ "chunks_file": "chunks.txt",
+ "video_file": "video{}",
+ "pre": {"work": "»", "done": "•", "skip": "~"},
+ "probe_py": "probe.py",
+ "mixdown_py": "mixdown.py",
+ "video_py": "video.py",
+}
+
+LOGGER = lg.getLogger(CONFIG["logger"])
+LOGLEV = [lg.INFO, lg.DEBUG]
+LOGLEV = [None] + sorted(LOGLEV, reverse=True)
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/helpers.py b/power_sequencer/scripts/BPSRender/bpsrender/helpers.py
new file mode 100644
index 00000000..9ebcf2b0
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/helpers.py
@@ -0,0 +1,110 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+from collections import deque
+from shutil import which
+
+
+class BSError(Exception):
+ """
+ Custom Exception raised if Blender is called with a python script argument
+ and gives error while trying to execute the script.
+ """
+
+ pass
+
+
+class ToolError(Exception):
+ """Raised if external dependencies aren't found on system.
+ """
+
+ pass
+
+
+def checktools(tools):
+ tools = [(t, which(t) or "") for t in tools]
+ check = {"tools": tools, "test": all(map(lambda x: x[1], tools))}
+ if not check["test"]:
+ msg = ["BPSRender couldn't find external dependencies:"]
+ msg += [
+ "[{check}] {tool}: {path}".format(
+ check="v" if path is not "" else "X", tool=tool, path=path or "NOT FOUND"
+ )
+ for tool, path in check["tools"]
+ ]
+ msg += [
+ (
+ "Check if you have them properly installed and available in the PATH"
+ " environemnt variable."
+ ),
+ "Exiting...",
+ ]
+ raise ToolError("\n".join(msg))
+
+
+def checkblender(what, search, cp, s):
+ """
+ IMPURE
+ Check Blender output for python script execution error.
+
+ Parameters
+ ----------
+ what: str
+ A tag used in the exception message.
+ search: iter(str)
+ One or more string(s) to search for in Blender's output.
+ cp: Popen
+ Blender subprocess.
+ s: PIPE
+ Blender's output.
+
+ Returns
+ -------
+ out: PIPE
+ The same pipe `s` is returned so that it can be iterated over on later
+ steps.
+ """
+ if not isinstance(search, list):
+ search = [search]
+ for search_item in search:
+ if search_item in s:
+ message = (
+ "Script {what} was not properly executed in" " Blender".format(what=what),
+ "CMD: {cmd}".format(what=what, cmd=" ".join(cp.args)),
+ "DUMP:".format(what=what),
+ s,
+ )
+ raise BSError("\n".join(message))
+ return s
+
+
+def printw(cfg, text, s="\n", e="...", p="", **kwargs):
+ p = p or cfg["pre"]["work"]
+ print("{s}{p} {}{e}".format(text, s=s, e=e, p=p), **kwargs)
+
+
+def printd(cfg, text, s="", e=".", p="", **kwargs):
+ p = p or cfg["pre"]["done"]
+ printw(cfg, text, s=s, e=e, p=p, **kwargs)
+
+
+def prints(cfg, text, s="", e=".", p="", **kwargs):
+ p = p or cfg["pre"]["skip"]
+ printw(cfg, text, s=s, e=e, p=p, **kwargs)
+
+
+def kickstart(it):
+ deque(it, maxlen=0)
diff --git a/power_sequencer/scripts/BPSRender/bpsrender/setup.py b/power_sequencer/scripts/BPSRender/bpsrender/setup.py
new file mode 100644
index 00000000..aba30d07
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/bpsrender/setup.py
@@ -0,0 +1,182 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+# IMPURE
+import logging as lg
+import os
+import os.path as osp
+from functools import reduce
+from itertools import starmap
+
+from .calls import call
+from .commands import get_commands, get_commands_all
+from .config import LOGGER, LOGLEV
+from .helpers import kickstart
+
+
+def setup_bspy(cfg, clargs, **kwargs):
+ """
+ Normalize the names of the script to be ran in Blender for certain steps.
+ Eg. the probe step depends on the script located in
+ `bpsrender/cfg['probe_py']`.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+
+ Returns
+ -------
+ out: dict
+ Dictoinary to be used in call steps.
+ """
+ out = filter(lambda x: x[0].endswith("_py"), cfg.items())
+ out = starmap(lambda k, v: ("{}_normalized".format(k), osp.join(cfg["bs_path"], v)), out)
+ return dict(out)
+
+
+def setup_probe(cfg, clargs, **kwargs):
+ """
+ IMPURE
+ Call Blender and extract information that will be necessary for later
+ steps.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY -- see individual functions for the list of mandatory keys
+ Dictionary with additional information from the previous setup step.
+
+ Returns
+ -------
+ out: dict
+ Dictoinary to be used in call steps.
+ """
+ return call(cfg, clargs, get_commands(cfg, clargs, "probe", **kwargs), **kwargs)
+
+
+def setup_paths(cfg, clargs, **kwargs):
+ """
+ Figure out appropriate path locations to store output for parts and final
+ render.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ MANDATORY -- see individual functions for the list of mandatory keys
+ Dictionary with additional information from the previous setup step.
+
+ Returns
+ -------
+ out: dict
+ Dictionary storing all relevant information pertaining to folder and file
+ paths.
+
+ Note
+ ----
+ It also creates the folder structure 'render/parts' where
+ `clargs.blendfile` is stored on disk.
+ """
+ render_parts_path = osp.join(clargs.output, cfg["parts_folder"])
+ name = osp.splitext(osp.basename(clargs.blendfile))[0]
+ render_mixdown_path = osp.join(render_parts_path, "{}_m.flac".format(name))
+ render_chunk_path = osp.join(render_parts_path, "{}_c_{}".format(name, "#" * cfg["frame_pad"]))
+ render_video_path = osp.join(render_parts_path, "{}_v{}".format(name, kwargs["ext"]))
+ render_audiovideo_path = osp.join(clargs.output, "{}{}".format(name, kwargs["ext"]))
+ chunks_file_path = osp.join(render_parts_path, cfg["chunks_file"])
+
+ out = {
+ "render_path": clargs.output,
+ "render_parts_path": render_parts_path,
+ "chunks_file_path": chunks_file_path,
+ "render_chunk_path": render_chunk_path,
+ "render_video_path": render_video_path,
+ "render_mixdown_path": render_mixdown_path,
+ "render_audiovideo_path": render_audiovideo_path,
+ }
+ return out
+
+
+def setup_folders_hdd(cfg, clargs, **kwargs):
+ """
+ IMPURE
+ Prepares the folder structure `cfg['render']/cfg['parts']'`.
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ Dictionary with additional information from the previous setup step.
+
+ Returns
+ -------
+ out: (iter((str, iter(tuple))), dict)
+ 1st element: see commands.py:get_commands_all
+ 2nd elment: the keyword arguments used by calls.py:call
+ """
+ # create folder structure if it doesn't exist already only if
+ # appropriate command line arguments are given
+ do_it = filter(lambda x: x[0].endswith("_only"), vars(clargs).items())
+ do_it = all(map(lambda x: not x[1], do_it))
+ do_it = not clargs.dry_run and clargs.video_only or clargs.mixdown_only or do_it
+ do_it and os.makedirs(kwargs["render_parts_path"], exist_ok=True)
+ return {}
+
+
+def setup(cfg, clargs):
+ """
+ IMPURE -- setup_paths
+ Prepares the folder structure 'render/parts', the appropriate command lists
+ to be called and the keyword arguments to be passed to call functions
+ (calls.py).
+
+ Parameters
+ ----------
+ cfg: dict
+ Configuration dictionary.
+ clargs: Namespace
+ Command line arguments (normalized).
+ kwargs: dict
+ Dictionary with additional information from the previous setup step.
+
+ Returns
+ -------
+ out: (iter((str, iter(tuple))), dict)
+ 1st element: see commands.py:get_commands_all
+ 2nd elment: the keyword arguments used by calls.py:call
+ """
+ setups_f = (setup_bspy, setup_probe, setup_paths, setup_folders_hdd)
+ lg.basicConfig(level=LOGLEV[min(clargs.verbose, len(LOGLEV) - 1)])
+
+ kwargs = dict(reduce(lambda acc, sf: {**acc, **sf(cfg, clargs, **acc)}, setups_f, {}))
+
+ LOGGER.info("Setup:")
+ kickstart(starmap(lambda k, v: LOGGER.info("{}: {}".format(k, v)), kwargs.items()))
+ return get_commands_all(cfg, clargs, **kwargs), kwargs
diff --git a/power_sequencer/scripts/BPSRender/setup.py b/power_sequencer/scripts/BPSRender/setup.py
new file mode 100644
index 00000000..4c4a74b9
--- /dev/null
+++ b/power_sequencer/scripts/BPSRender/setup.py
@@ -0,0 +1,55 @@
+#
+# Copyright (C) 2016-2019 by Razvan Radulescu, Nathan Lovato, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+from setuptools import setup
+
+
+def readme():
+ with open("README.rst") as f:
+ return f.read()
+
+
+setup(
+ name="bpsrender",
+ version="0.1.40.post1",
+ description="Blender Power Sequencer Renderer",
+ long_description=readme(),
+ classifiers=[
+ "Development Status :: 4 - Beta",
+ "Environment :: Console",
+ "Intended Audience :: End Users/Desktop",
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+ "Natural Language :: English",
+ "Programming Language :: Python :: 3.3",
+ "Programming Language :: Python :: 3.4",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: 3.6",
+ "Programming Language :: Python :: 3.7",
+ "Topic :: Text Processing :: Linguistic",
+ "Topic :: Multimedia :: Video",
+ "Topic :: Utilities",
+ ],
+ url="https://gitlab.com/razcore/BPSRender",
+ keywords="blender render parallel multiprocess speedup utility" " productivty",
+ author="Răzvan C. Rădulescu",
+ author_email="razcore.art@gmail.com",
+ license="GPLv3",
+ packages=["bpsrender"],
+ install_requires=["tqdm"],
+ zip_safe=False,
+ entry_points={"console_scripts": ["bpsrender=bpsrender.__main__:main"]},
+ include_package_data=True,
+)
diff --git a/power_sequencer/ui/__init__.py b/power_sequencer/ui/__init__.py
new file mode 100644
index 00000000..61edcb35
--- /dev/null
+++ b/power_sequencer/ui/__init__.py
@@ -0,0 +1,48 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from .menu_contextual import POWER_SEQUENCER_MT_contextual
+from .menu_toolbar import (
+ POWER_SEQUENCER_MT_main,
+ POWER_SEQUENCER_MT_playback,
+ POWER_SEQUENCER_MT_strips,
+ POWER_SEQUENCER_MT_select,
+ POWER_SEQUENCER_MT_edit,
+ POWER_SEQUENCER_MT_markers,
+ POWER_SEQUENCER_MT_file,
+ POWER_SEQUENCER_MT_trim,
+ POWER_SEQUENCER_MT_preview,
+ POWER_SEQUENCER_MT_audio,
+ POWER_SEQUENCER_MT_transitions,
+)
+
+classes = [
+ POWER_SEQUENCER_MT_contextual,
+ POWER_SEQUENCER_MT_main,
+ POWER_SEQUENCER_MT_playback,
+ POWER_SEQUENCER_MT_strips,
+ POWER_SEQUENCER_MT_select,
+ POWER_SEQUENCER_MT_edit,
+ POWER_SEQUENCER_MT_markers,
+ POWER_SEQUENCER_MT_file,
+ POWER_SEQUENCER_MT_trim,
+ POWER_SEQUENCER_MT_preview,
+ POWER_SEQUENCER_MT_audio,
+ POWER_SEQUENCER_MT_transitions,
+]
+
+register_ui, unregister_ui = bpy.utils.register_classes_factory(classes)
diff --git a/power_sequencer/ui/menu_contextual.py b/power_sequencer/ui/menu_contextual.py
new file mode 100644
index 00000000..6ba0640a
--- /dev/null
+++ b/power_sequencer/ui/menu_contextual.py
@@ -0,0 +1,83 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+from ..operators.utils.global_settings import SequenceTypes
+
+
+class POWER_SEQUENCER_MT_contextual(bpy.types.Menu):
+ bl_label = "Power Sequencer"
+ # bl_idname = "SEQUENCER_MT_power_sequencer_menu"
+
+ def draw(self, context):
+ layout = self.layout
+
+ if not bpy.data.is_saved:
+ layout.label("Please save your project")
+ layout.operator("wm.save_as_mainfile", icon="SAVE_AS", text="Save as")
+ return
+
+ if not context.sequences:
+ layout.operator(
+ "power_sequencer.import_local_footage", icon="SEQUENCE", text="Import local footage"
+ )
+ return
+
+ selection = context.selected_sequences
+ active_strip = context.scene.sequence_editor.active_strip
+ types = set([s.type for s in selection])
+
+ if active_strip.type == "GAMMA_CROSS":
+ layout.operator(
+ "power_sequencer.crossfade_edit", icon="ACTION_TWEAK", text="Edit crossfade"
+ )
+
+ for t in types:
+ if t in SequenceTypes.VIDEO:
+ layout.operator("power_sequencer.fade_add", icon="IMAGE_ALPHA", text="Fade strips")
+ break
+
+ if len(selection) == 1:
+ for s in [active_strip, selection[0]]:
+ if s.type in SequenceTypes.VIDEO or s.type in SequenceTypes.IMAGE:
+ layout.separator()
+ layout.operator(
+ "power_sequencer.crossfade_add", icon="IMAGE_ALPHA", text="Auto crossfade"
+ )
+ break
+
+ # TODO: Doesn't work from the menu, I guess there's an issue with the invoke method?
+ # layout.separator()
+ # layout.operator('power_sequencer.grab_closest_handle_or_cut', icon='SNAP_SURFACE', text='Grab cut or handle')
+
+ if len(selection) > 1:
+ layout.separator()
+
+ layout.operator(
+ "power_sequencer.ripple_delete", icon="AUTOMERGE_ON", text="Ripple delete"
+ )
+ layout.operator(
+ "power_sequencer.snap_selection", icon="SNAP_ON", text="Snap selection"
+ )
+
+ layout.separator()
+
+ layout.operator(
+ "power_sequencer.import_local_footage", icon="SEQUENCE", text="Import local footage"
+ )
+ layout.operator(
+ "power_sequencer.render_video", icon="RENDER_ANIMATION", text="Render video for the web"
+ )
diff --git a/power_sequencer/ui/menu_toolbar.py b/power_sequencer/ui/menu_toolbar.py
new file mode 100644
index 00000000..cb647da5
--- /dev/null
+++ b/power_sequencer/ui/menu_toolbar.py
@@ -0,0 +1,204 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+
+
+class POWER_SEQUENCER_MT_main(bpy.types.Menu):
+ bl_label = "Power Sequencer"
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.separator()
+
+ layout.menu("POWER_SEQUENCER_MT_file")
+ layout.menu("POWER_SEQUENCER_MT_edit")
+ layout.menu("POWER_SEQUENCER_MT_select")
+ layout.menu("POWER_SEQUENCER_MT_trim")
+ layout.menu("POWER_SEQUENCER_MT_strips")
+ layout.menu("POWER_SEQUENCER_MT_transitions")
+ layout.menu("POWER_SEQUENCER_MT_audio")
+ layout.menu("POWER_SEQUENCER_MT_playback")
+ layout.menu("POWER_SEQUENCER_MT_preview")
+ layout.menu("POWER_SEQUENCER_MT_markers")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.render_apply_preset", text="Apply Render Preset")
+
+
+class POWER_SEQUENCER_MT_playback(bpy.types.Menu):
+ bl_label = "Playback"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("power_sequencer.playback_speed_increase")
+ layout.operator("power_sequencer.playback_speed_decrease")
+ layout.operator("power_sequencer.playback_speed_set")
+
+
+class POWER_SEQUENCER_MT_strips(bpy.types.Menu):
+ bl_label = "Strips"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("power_sequencer.speed_up_movie_strip")
+ layout.operator("power_sequencer.speed_remove_effect")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.concatenate_strips")
+ layout.operator("power_sequencer.swap_strips")
+ layout.operator("power_sequencer.toggle_selected_mute")
+ layout.operator("power_sequencer.channel_offset")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.make_still_image")
+
+
+class POWER_SEQUENCER_MT_transitions(bpy.types.Menu):
+ bl_label = "Transitions"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("power_sequencer.fade_add")
+ layout.operator("power_sequencer.fade_clear")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.crossfade_add")
+ layout.operator("power_sequencer.crossfade_edit")
+ layout.operator("power_sequencer.transitions_remove")
+
+
+class POWER_SEQUENCER_MT_select(bpy.types.Menu):
+ bl_label = "Select"
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.operator("power_sequencer.select_linked_effect")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.deselect_all_strips_left_or_right")
+ layout.operator("power_sequencer.deselect_handles_and_grab")
+
+
+class POWER_SEQUENCER_MT_edit(bpy.types.Menu):
+ bl_label = "Edit"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("power_sequencer.delete_direct")
+ layout.operator("power_sequencer.ripple_delete")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.gap_remove")
+ layout.operator(
+ "power_sequencer.copy_selected_sequences", text="Copy Selected", icon="COPYDOWN"
+ )
+
+ layout.separator()
+
+ layout.operator("power_sequencer.grab")
+ layout.operator("power_sequencer.grab_closest_cut")
+ layout.operator("power_sequencer.grab_sequence_handles")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.trim_left_or_right_handles")
+ layout.operator("power_sequencer.snap_selection")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.scene_cycle")
+
+
+class POWER_SEQUENCER_MT_markers(bpy.types.Menu):
+ bl_label = "Markers"
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.operator("power_sequencer.marker_delete_closest")
+ layout.operator("power_sequencer.marker_delete_direct")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.synchronize_titles")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.marker_go_to_next")
+ layout.operator("power_sequencer.copy_markers_as_timecodes")
+ layout.operator("power_sequencer.marker_snap_to_cursor")
+ layout.operator("power_sequencer.set_preview_between_markers")
+ layout.operator("power_sequencer.markers_snap_matching_strips")
+
+
+class POWER_SEQUENCER_MT_file(bpy.types.Menu):
+ bl_label = "File"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator(
+ "power_sequencer.open_project_directory",
+ text="Open Project Directory",
+ icon="FILE_FOLDER",
+ )
+ layout.operator("power_sequencer.save_direct")
+ layout.operator("power_sequencer.import_local_footage")
+
+
+class POWER_SEQUENCER_MT_trim(bpy.types.Menu):
+ bl_label = "Trim"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("power_sequencer.trim_three_point_edit")
+ layout.operator("power_sequencer.trim_to_surrounding_cuts")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.mouse_trim")
+ layout.operator("power_sequencer.mouse_trim_instantly")
+
+
+class POWER_SEQUENCER_MT_preview(bpy.types.Menu):
+ bl_label = "Preview"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("power_sequencer.preview_closest_cut")
+ layout.operator("power_sequencer.preview_to_selection")
+
+ layout.separator()
+
+ layout.operator("power_sequencer.set_timeline_range")
+
+
+class POWER_SEQUENCER_MT_audio(bpy.types.Menu):
+ bl_label = "Audio"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator("power_sequencer.align_audios")
+ layout.operator("power_sequencer.toggle_waveforms")
+ layout.operator("power_sequencer.mouse_toggle_mute")
diff --git a/power_sequencer/utils/addon_auto_imports.py b/power_sequencer/utils/addon_auto_imports.py
new file mode 100644
index 00000000..e570f53c
--- /dev/null
+++ b/power_sequencer/utils/addon_auto_imports.py
@@ -0,0 +1,72 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import pkgutil
+import importlib
+
+reload_event = False
+
+
+def setup_addon_modules(path, package_name, ignore_packages=[], ignore_modules=[]):
+ """
+ Imports and reloads all modules in this addon.
+
+ path -- __path__ from __init__.py
+ package_name -- __name__ from __init__.py
+ ignore_packages -- list of packages to ignore,
+ skips the package if the string is in the name
+ ignore_modules -- list of module_names to ignore, strings
+ skips the module if the string is in the name
+ """
+
+ def get_submodule_names(path=path[0], root=""):
+ module_names = []
+ for ignore in ignore_packages:
+ if ignore in root:
+ return []
+ for importer, module_name, is_package in pkgutil.iter_modules([path]):
+ skip_module = False
+ for ignore in ignore_modules:
+ if ignore in module_name:
+ skip_module = True
+ if skip_module:
+ continue
+ if is_package:
+ sub_path = path + "\\" + module_name
+ sub_root = root + module_name + "."
+ module_names.extend(get_submodule_names(sub_path, sub_root))
+ else:
+ module_names.append(root + module_name)
+ return module_names
+
+ def import_submodules(names):
+ modules = []
+ for name in names:
+ modules.append(importlib.import_module("." + name, package_name))
+ return modules
+
+ def reload_modules(modules):
+ for module in modules:
+ importlib.reload(module)
+
+ names = get_submodule_names()
+ modules = import_submodules(names)
+ if reload_event:
+ reload_modules(modules)
+ return modules
+
+
+reload_event = True
diff --git a/power_sequencer/utils/register_shortcuts.py b/power_sequencer/utils/register_shortcuts.py
new file mode 100644
index 00000000..074fb1c3
--- /dev/null
+++ b/power_sequencer/utils/register_shortcuts.py
@@ -0,0 +1,62 @@
+#
+# Copyright (C) 2016-2019 by Nathan Lovato, Daniel Oakey, Razvan Radulescu, and contributors
+#
+# This file is part of Power Sequencer.
+#
+# Power Sequencer is free software: you can redistribute it and/or modify it under the terms of the
+# GNU General Public License as published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# Power Sequencer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with Power Sequencer. If
+# not, see <https://www.gnu.org/licenses/>.
+#
+import bpy
+import operator as op
+from .. import operators
+from itertools import groupby
+
+
+keymaps_meta = {"Frames": "EMPTY", "Sequencer": "SEQUENCE_EDITOR", "Markers": "EMPTY"}
+
+
+def set_keymap_property(properties, property_name, value):
+ try:
+ setattr(properties, property_name, value)
+ except AttributeError:
+ print(
+ "Warning: property '%s' not found in keymap item '%s'"
+ % (property_name, properties.__class__.__name__)
+ )
+ except Exception as e:
+ print("Warning: %r" % e)
+
+
+def register_shortcuts():
+ def keymapgetter(operator):
+ return operator[1]["keymap"]
+
+ data = dir(operators)
+ data = filter(lambda operator: operator[0].isupper(), data)
+ data = map(lambda operator: op.attrgetter(operator), data)
+ data = map(lambda operator: operator(operators), data)
+ data = map(lambda operator: op.attrgetter("bl_idname", "doc")(operator), data)
+ data = {k: v for k, v in data if v != {}}
+ data.update(operators.doc)
+ data = sorted(data.items(), key=keymapgetter)
+ data = groupby(data, key=keymapgetter)
+
+ kms = []
+ wm = bpy.context.window_manager
+ for name, group in data:
+ km = wm.keyconfigs.addon.keymaps.new(name=name, space_type=keymaps_meta[name])
+ for bl_idname, d in group:
+ for s in d["shortcuts"]:
+ kmi = km.keymap_items.new(bl_idname, **s[0])
+ for pn, pv in s[1].items():
+ set_keymap_property(kmi.properties, pn, pv)
+ kms.append((km, kmi))
+ return kms