diff options
author | Nathan Lovato <nathan@gdquest.com> | 2019-09-05 18:22:37 +0300 |
---|---|---|
committer | Nathan Lovato <nathan@gdquest.com> | 2019-09-05 18:22:52 +0300 |
commit | 61d48c0a4be0ab8f71e6e1d35f0aa99c77fcfd33 (patch) | |
tree | f64d0ea88789184f45112b489598990d549788f2 /power_sequencer/operators | |
parent | da5a1175e30c347fbce05e49e2f5f895be30bd5b (diff) |
Add the VSE addon Power Sequencer
Diffstat (limited to 'power_sequencer/operators')
89 files changed, 7976 insertions, 0 deletions
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) |