diff --git a/docs/gallery.rst b/docs/gallery.rst index db5e355d5..2c5827b62 100644 --- a/docs/gallery.rst +++ b/docs/gallery.rst @@ -19,9 +19,7 @@ This mix of 60 covers of the Cup Song demonstrates the non-linear video editing
- +
The (old) MoviePy reel video. @@ -33,8 +31,7 @@ in the :ref:`examples`. .. raw:: html
-
@@ -129,8 +126,7 @@ This `transcribing piano rolls blog post - @@ -171,8 +167,7 @@ Videogrep is a python script written by Sam Lavigne, that goes through the subti .. raw:: html
-
@@ -200,12 +195,5 @@ This `Videogrep blog post -This `other post `_ uses MoviePy to automatically cut together all the highlights of a soccer game, based on the fact that the crowd cheers louder when something interesting happens. All in under 30 lines of Python: - -.. raw:: html +This `other post `_ uses MoviePy to automatically cut together `all the highlights of a soccer game `_, based on the fact that the crowd cheers louder when something interesting happens. All in under 30 lines of Python: -
- -
diff --git a/docs/ref/audiofx.rst b/docs/ref/audiofx.rst index 80f036cdc..a9a214493 100644 --- a/docs/ref/audiofx.rst +++ b/docs/ref/audiofx.rst @@ -19,7 +19,8 @@ You can either import a single function like this: :: Or import everything: :: import moviepy.audio.fx.all as afx - newaudio = (audioclip.afx( vfx.volumex, 0.5) + newaudio = (audioclip.afx( vfx.normalize) + .afx( vfx.volumex, 0.5) .afx( vfx.audio_fadein, 1.0) .afx( vfx.audio_fadeout, 1.0)) @@ -41,4 +42,5 @@ the module ``audio.fx`` is loaded as ``afx`` and you can use ``afx.volumex``, et audio_fadein audio_fadeout audio_loop - volumex \ No newline at end of file + audio_normalize + volumex diff --git a/docs/ref/audiofx/moviepy.audio.fx.all.audio_normalize.rst b/docs/ref/audiofx/moviepy.audio.fx.all.audio_normalize.rst new file mode 100644 index 000000000..a5cc3c771 --- /dev/null +++ b/docs/ref/audiofx/moviepy.audio.fx.all.audio_normalize.rst @@ -0,0 +1,6 @@ +moviepy.audio.fx.all.audio_normalize +================================== + +.. currentmodule:: moviepy.audio.fx.all + +.. autofunction:: audio_normalize diff --git a/moviepy/Clip.py b/moviepy/Clip.py index 69a3ffff4..015ecf513 100644 --- a/moviepy/Clip.py +++ b/moviepy/Clip.py @@ -18,31 +18,31 @@ class Clip: """ - + Base class of all clips (VideoClips and AudioClips). - - + + Attributes ----------- - + start: When the clip is included in a composition, time of the - composition at which the clip starts playing (in seconds). - + composition at which the clip starts playing (in seconds). + end: When the clip is included in a composition, time of the composition at which the clip starts playing (in seconds). - + duration: Duration of the clip (in seconds). Some clips are infinite, in this case their duration will be ``None``. - + """ - + # prefix for all temporary video and audio files. # You can overwrite it with # >>> Clip._TEMP_FILES_PREFIX = "temp_" - + _TEMP_FILES_PREFIX = 'TEMP_MPY_' def __init__(self): @@ -50,7 +50,7 @@ def __init__(self): self.start = 0 self.end = None self.duration = None - + self.memoize = False self.memoized_t = None self.memoize_frame = None @@ -62,12 +62,12 @@ def copy(self): Returns a shallow copy of the clip whose mask and audio will be shallow copies of the clip's mask and audio if they exist. - + This method is intensively used to produce new clips every time there is an outplace transformation of the clip (clip.resize, clip.subclip, etc.) """ - + newclip = copy(self) if hasattr(self, 'audio'): newclip.audio = copy(self.audio) @@ -75,14 +75,14 @@ def copy(self): newclip.mask = copy(self.mask) return newclip - + @convert_to_seconds(['t']) def get_frame(self, t): """ Gets a numpy array representing the RGB picture of the clip at time t or (mono or stereo) value for a sound clip """ - # Coming soon: smart error handling for debugging at this point + # Coming soon: smart error handling for debugging at this point if self.memoize: if t == self.memoized_t: return self.memoized_frame @@ -99,48 +99,48 @@ def fl(self, fun, apply_to=None, keep_duration=True): Returns a new Clip whose frames are a transformation (through function ``fun``) of the frames of the current clip. - + Parameters ----------- - + fun A function with signature (gf,t -> frame) where ``gf`` will represent the current clip's ``get_frame`` method, i.e. ``gf`` is a function (t->image). Parameter `t` is a time in seconds, `frame` is a picture (=Numpy array) which will be returned by the transformed clip (see examples below). - + apply_to Can be either ``'mask'``, or ``'audio'``, or ``['mask','audio']``. Specifies if the filter ``fl`` should also be applied to the audio or the mask of the clip, if any. - + keep_duration Set to True if the transformation does not change the ``duration`` of the clip. - + Examples -------- - + In the following ``newclip`` a 100 pixels-high clip whose video content scrolls from the top to the bottom of the frames of ``clip``. - + >>> fl = lambda gf,t : gf(t)[int(t):int(t)+50, :] >>> newclip = clip.fl(fl, apply_to='mask') - + """ if apply_to is None: apply_to = [] #mf = copy(self.make_frame) newclip = self.set_make_frame(lambda t: fun(self.get_frame, t)) - + if not keep_duration: newclip.duration = None newclip.end = None - + if isinstance(apply_to, str): apply_to = [apply_to] @@ -150,76 +150,76 @@ def fl(self, fun, apply_to=None, keep_duration=True): if a is not None: new_a = a.fl(fun, keep_duration=keep_duration) setattr(newclip, attr, new_a) - + return newclip - - + + def fl_time(self, t_func, apply_to=None, keep_duration=False): """ Returns a Clip instance playing the content of the current clip but with a modified timeline, time ``t`` being replaced by another time `t_func(t)`. - + Parameters ----------- - + t_func: A function ``t-> new_t`` - + apply_to: Can be either 'mask', or 'audio', or ['mask','audio']. Specifies if the filter ``fl`` should also be applied to the audio or the mask of the clip, if any. - + keep_duration: ``False`` (default) if the transformation modifies the ``duration`` of the clip. - + Examples -------- - + >>> # plays the clip (and its mask and sound) twice faster >>> newclip = clip.fl_time(lambda: 2*t, apply_to=['mask', 'audio']) >>> >>> # plays the clip starting at t=3, and backwards: >>> newclip = clip.fl_time(lambda: 3-t) - + """ if apply_to is None: apply_to = [] - + return self.fl(lambda gf, t: gf(t_func(t)), apply_to, keep_duration=keep_duration) - - - + + + def fx(self, func, *args, **kwargs): """ - + Returns the result of ``func(self, *args, **kwargs)``. for instance - + >>> newclip = clip.fx(resize, 0.2, method='bilinear') - + is equivalent to - + >>> newclip = resize(clip, 0.2, method='bilinear') - + The motivation of fx is to keep the name of the effect near its parameters, when the effects are chained: - + >>> from moviepy.video.fx import volumex, resize, mirrorx >>> clip.fx( volumex, 0.5).fx( resize, 0.3).fx( mirrorx ) >>> # Is equivalent, but clearer than >>> resize( volumex( mirrorx( clip ), 0.5), 0.3) - + """ - + return func(self, *args, **kwargs) - - - + + + @apply_to_mask @apply_to_audio @convert_to_seconds(['t']) @@ -230,27 +230,27 @@ def set_start(self, t, change_end=True): to ``t``, which can be expressed in seconds (15.35), in (min, sec), in (hour, min, sec), or as a string: '01:03:05.35'. - + If ``change_end=True`` and the clip has a ``duration`` attribute, the ``end`` atrribute of the clip will be updated to ``start+duration``. - + If ``change_end=False`` and the clip has a ``end`` attribute, - the ``duration`` attribute of the clip will be updated to + the ``duration`` attribute of the clip will be updated to ``end-start`` - + These changes are also applied to the ``audio`` and ``mask`` clips of the current clip, if they exist. """ - + self.start = t if (self.duration is not None) and change_end: self.end = t + self.duration elif (self.end is not None): self.duration = self.end - self.start - - - + + + @apply_to_mask @apply_to_audio @convert_to_seconds(['t']) @@ -264,6 +264,7 @@ def set_end(self, t): of the returned clip. """ self.end = t + if self.end is None: return if self.start is None: if self.duration is not None: self.start = max(0, t - newclip.duration) @@ -271,7 +272,7 @@ def set_end(self, t): self.duration = self.end - self.start - + @apply_to_mask @apply_to_audio @convert_to_seconds(['t']) @@ -292,9 +293,9 @@ def set_duration(self, t, change_end=True): if change_end: self.end = None if (t is None) else (self.start + t) else: - if duration is None: + if self.duration is None: raise Exception("Cannot change clip start when new" - "duration is None") + "duration is None") self.start = self.end - t @@ -309,53 +310,53 @@ def set_make_frame(self, make_frame): @outplace def set_fps(self, fps): """ Returns a copy of the clip with a new default fps for functions like - write_videofile, iterframe, etc. """ + write_videofile, iterframe, etc. """ self.fps = fps @outplace def set_ismask(self, ismask): - """ Says wheter the clip is a mask or not (ismask is a boolean)""" + """ Says wheter the clip is a mask or not (ismask is a boolean)""" self.ismask = ismask @outplace def set_memoize(self, memoize): - """ Sets wheter the clip should keep the last frame read in memory """ - self.memoize = memoize - + """ Sets wheter the clip should keep the last frame read in memory """ + self.memoize = memoize + @convert_to_seconds(['t']) def is_playing(self, t): """ - + If t is a time, returns true if t is between the start and the end of the clip. t can be expressed in seconds (15.35), in (min, sec), in (hour, min, sec), or as a string: '01:03:05.35'. If t is a numpy array, returns False if none of the t is in theclip, else returns a vector [b_1, b_2, b_3...] where b_i - is true iff tti is in the clip. + is true iff tti is in the clip. """ - + if isinstance(t, np.ndarray): # is the whole list of t outside the clip ? tmin, tmax = t.min(), t.max() - + if (self.end is not None) and (tmin >= self.end) : return False - + if tmax < self.start: return False - + # If we arrive here, a part of t falls in the clip result = 1 * (t >= self.start) if (self.end is not None): result *= (t <= self.end) return result - + else: - + return( (t >= self.start) and ((self.end is None) or (t < self.end) ) ) - + @convert_to_seconds(['t_start', 't_end']) @@ -371,13 +372,13 @@ def subclip(self, t_start=0, t_end=None): of the clip (potentially infinite). If ``t_end`` is a negative value, it is reset to ``clip.duration + t_end. ``. For instance: :: - + >>> # cut the last two seconds of the clip: >>> newclip = clip.subclip(0,-2) - + If ``t_end`` is provided or if the clip has a duration attribute, the duration of the returned clip is set automatically. - + The ``mask`` and ``audio`` of the resulting subclip will be subclips of ``mask`` and ``audio`` the original clip, if they exist. @@ -388,7 +389,6 @@ def subclip(self, t_start=0, t_end=None): t_start = self.duration + t_start #remeber t_start is negative if (self.duration is not None) and (t_start>self.duration): - raise ValueError("t_start (%.02f) "%t_start + "should be smaller than the clip's "+ "duration (%.02f)."%self.duration) @@ -396,28 +396,28 @@ def subclip(self, t_start=0, t_end=None): newclip = self.fl_time(lambda t: t + t_start, apply_to=[]) if (t_end is None) and (self.duration is not None): - + t_end = self.duration - + elif (t_end is not None) and (t_end<0): - + if self.duration is None: - + print ("Error: subclip with negative times (here %s)"%(str((t_start, t_end))) +" can only be extracted from clips with a ``duration``") - + else: - + t_end = self.duration + t_end - + if (t_end is not None): - + newclip.duration = t_end - t_start newclip.end = newclip.start + newclip.duration - + return newclip - + @apply_to_mask @apply_to_audio @convert_to_seconds(['ta', 'tb']) @@ -430,20 +430,20 @@ def cutout(self, ta, tb): If the original clip has a ``duration`` attribute set, the duration of the returned clip is automatically computed as `` duration - (tb - ta)``. - + The resulting clip's ``audio`` and ``mask`` will also be cutout if they exist. """ - + fl = lambda t: t + (t >= ta)*(tb - ta) newclip = self.fl_time(fl) - + if self.duration is not None: - + return newclip.set_duration(self.duration - (tb - ta)) - + else: - + return newclip @requires_duration @@ -451,22 +451,22 @@ def cutout(self, ta, tb): def iter_frames(self, fps=None, with_times = False, progress_bar=False, dtype=None): """ Iterates over all the frames of the clip. - + Returns each frame of the clip as a HxWxN np.array, where N=1 for mask clips and N=3 for RGB clips. - + This function is not really meant for video editing. It provides an easy way to do frame-by-frame treatment of a video, for fields like science, computer vision... - + The ``fps`` (frames per second) parameter is optional if the clip already has a ``fps`` attribute. - Use dtype="uint8" when using the pictures to write video, images... - + Use dtype="uint8" when using the pictures to write video, images... + Examples --------- - + >>> # prints the maximum of red that is contained >>> # on the first line of each frame of the clip. >>> from moviepy.editor import VideoFileClip @@ -484,7 +484,7 @@ def generator(): yield t, frame else: yield frame - + if progress_bar: nframes = int(self.duration*fps)+1 return tqdm(generator(), total=nframes) diff --git a/moviepy/audio/fx/audio_normalize.py b/moviepy/audio/fx/audio_normalize.py new file mode 100644 index 000000000..127e9ea64 --- /dev/null +++ b/moviepy/audio/fx/audio_normalize.py @@ -0,0 +1,9 @@ +from moviepy.decorators import audio_video_fx + +@audio_video_fx +def audio_normalize(clip): + """ Return an audio (or video) clip whose volume is normalized + to 0db.""" + + mv = clip.max_volume() + return clip.volumex(1 / mv) diff --git a/moviepy/editor.py b/moviepy/editor.py index 0bf2feeb8..f8a914082 100644 --- a/moviepy/editor.py +++ b/moviepy/editor.py @@ -53,6 +53,7 @@ for method in [ "afx.audio_fadein", "afx.audio_fadeout", + "afx.audio_normalize", "afx.volumex", "transfx.crossfadein", "transfx.crossfadeout", @@ -75,6 +76,7 @@ for method in ["afx.audio_fadein", "afx.audio_fadeout", "afx.audio_loop", + "afx.audio_normalize", "afx.volumex" ]: @@ -111,4 +113,4 @@ def preview(self, *args, **kwargs): """ NOT AVAILABLE : clip.preview requires Pygame installed.""" raise ImportError("clip.preview requires Pygame installed") -AudioClip.preview = preview \ No newline at end of file +AudioClip.preview = preview diff --git a/moviepy/video/compositing/CompositeVideoClip.py b/moviepy/video/compositing/CompositeVideoClip.py index 8d0012ed9..50fe69212 100644 --- a/moviepy/video/compositing/CompositeVideoClip.py +++ b/moviepy/video/compositing/CompositeVideoClip.py @@ -40,8 +40,7 @@ class CompositeVideoClip(VideoClip): have the same size as the final clip. If it has no transparency, the final clip will have no mask. - If all clips with a fps attribute have the same fps, it becomes the fps of - the result. + The clip with the highest FPS will be the FPS of the composite clip. """ @@ -60,10 +59,11 @@ def __init__(self, clips, size=None, bg_color=None, use_bgclip=False, if bg_color is None: bg_color = 0.0 if ismask else (0, 0, 0) - - fps_list = list(set([c.fps for c in clips if hasattr(c,'fps')])) - if len(fps_list)==1: - self.fps= fps_list[0] + fpss = [c.fps for c in clips if hasattr(c, 'fps') and c.fps is not None] + if len(fpss) == 0: + self.fps = None + else: + self.fps = max(fpss) VideoClip.__init__(self) @@ -97,7 +97,7 @@ def __init__(self, clips, size=None, bg_color=None, use_bgclip=False, # compute mask if necessary if transparent: maskclips = [(c.mask if (c.mask is not None) else - c.add_mask().mask).set_pos(c.pos) + c.add_mask().mask).set_pos(c.pos).set_end(c.end).set_start(c.start, change_end=False) for c in self.clips] self.mask = CompositeVideoClip(maskclips,self.size, ismask=True, diff --git a/moviepy/video/compositing/transitions.py b/moviepy/video/compositing/transitions.py index 0c7336339..a6837d7f5 100644 --- a/moviepy/video/compositing/transitions.py +++ b/moviepy/video/compositing/transitions.py @@ -43,7 +43,7 @@ def slide_in(clip, duration, side): Parameters =========== - + clip A video clip. @@ -53,10 +53,10 @@ def slide_in(clip, duration, side): side Side of the screen where the clip comes from. One of 'top' | 'bottom' | 'left' | 'right' - + Examples ========= - + >>> from moviepy.editor import * >>> clips = [... make a list of clips] >>> slided_clips = [clip.fx( transfx.slide_in, 1, 'left') @@ -69,7 +69,7 @@ def slide_in(clip, duration, side): 'right' : lambda t: (max(0,w*(1-t/duration)),'center'), 'top' : lambda t: ('center',min(0,h*(t/duration-1))), 'bottom': lambda t: ('center',max(0,h*(1-t/duration)))} - + return clip.set_pos( pos_dict[side] ) @@ -83,7 +83,7 @@ def slide_out(clip, duration, side): Parameters =========== - + clip A video clip. @@ -93,10 +93,10 @@ def slide_out(clip, duration, side): side Side of the screen where the clip goes. One of 'top' | 'bottom' | 'left' | 'right' - + Examples ========= - + >>> from moviepy.editor import * >>> clips = [... make a list of clips] >>> slided_clips = [clip.fx( transfx.slide_out, 1, 'bottom') @@ -106,12 +106,12 @@ def slide_out(clip, duration, side): """ w,h = clip.size - t_s = clip.duration - duration # start time of the effect. + ts = clip.duration - duration # start time of the effect. pos_dict = {'left' : lambda t: (min(0,w*(1-(t-ts)/duration)),'center'), 'right' : lambda t: (max(0,w*((t-ts)/duration-1)),'center'), 'top' : lambda t: ('center',min(0,h*(1-(t-ts)/duration))), 'bottom': lambda t: ('center',max(0,h*((t-ts)/duration-1))) } - + return clip.set_pos( pos_dict[side] ) @@ -119,7 +119,7 @@ def slide_out(clip, duration, side): def make_loopable(clip, cross_duration): """ Makes the clip fade in progressively at its own end, this way it can be looped indefinitely. ``cross`` is the duration in seconds - of the fade-in. """ + of the fade-in. """ d = clip.duration clip2 = clip.fx(crossfadein, cross_duration).\ set_start(d - cross_duration) diff --git a/moviepy/video/tools/cuts.py b/moviepy/video/tools/cuts.py index fbf5d05d9..5c3d449a5 100644 --- a/moviepy/video/tools/cuts.py +++ b/moviepy/video/tools/cuts.py @@ -274,7 +274,7 @@ def write_gifs(self, clip, gif_dir): @use_clip_fps_by_default def detect_scenes(clip=None, luminosities=None, thr=10, - progress_bar=False, fps=None): + progress_bar=True, fps=None): """ Detects scenes of a clip based on luminosity changes. Note that for large clip this may take some time @@ -320,7 +320,7 @@ def detect_scenes(clip=None, luminosities=None, thr=10, if luminosities is None: luminosities = [f.sum() for f in clip.iter_frames( - fps=fps, dtype='uint32', progress_bar=1)] + fps=fps, dtype='uint32', progress_bar=progress_bar)] luminosities = np.array(luminosities, dtype=float) if clip is not None: diff --git a/tests/test_PR.py b/tests/test_PR.py index f9ab93dca..3fda53da1 100644 --- a/tests/test_PR.py +++ b/tests/test_PR.py @@ -8,6 +8,7 @@ from moviepy.video.io.VideoFileClip import VideoFileClip from moviepy.video.tools.interpolators import Trajectory from moviepy.video.VideoClip import ColorClip, ImageClip, TextClip +from moviepy.video.compositing.CompositeVideoClip import CompositeVideoClip sys.path.append("tests") @@ -116,5 +117,18 @@ def test_PR_529(): assert video_clip.rotation == 180 +def test_PR_610(): + """ + Test that the max fps of the video clips is used for the composite video clip + """ + clip1 = ColorClip((640, 480), color=(255, 0, 0)).set_duration(1) + clip2 = ColorClip((640, 480), color=(0, 255, 0)).set_duration(1) + clip1.fps = 24 + clip2.fps = 25 + composite = CompositeVideoClip([clip1, clip2]) + + assert composite.fps == 25 + + if __name__ == '__main__': pytest.main() diff --git a/tests/test_fx.py b/tests/test_fx.py index 88f5f4280..22ba20f43 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -8,6 +8,8 @@ from moviepy.video.fx.crop import crop from moviepy.video.fx.fadein import fadein from moviepy.video.fx.fadeout import fadeout +from moviepy.audio.fx.audio_normalize import audio_normalize +from moviepy.audio.io.AudioFileClip import AudioFileClip from moviepy.video.io.VideoFileClip import VideoFileClip sys.path.append("tests") @@ -68,6 +70,11 @@ def test_fadeout(): clip1 = fadeout(clip, 1) clip1.write_videofile(os.path.join(TMP_DIR,"fadeout1.webm")) +def test_normalize(): + clip = AudioFileClip('media/crunching.mp3') + clip = audio_normalize(clip) + assert clip.max_volume() == 1 + if __name__ == '__main__': pytest.main() diff --git a/tests/test_issues.py b/tests/test_issues.py index 9c95ba982..e895f8128 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -269,6 +269,19 @@ def test_audio_reader(): subclip.write_audiofile(os.path.join(TMP_DIR, 'issue_246.wav'), write_logfile=True) +def test_issue_547(): + red = ColorClip((640, 480), color=(255,0,0)).set_duration(1) + green = ColorClip((640, 480), color=(0,255,0)).set_duration(2) + blue = ColorClip((640, 480), color=(0,0,255)).set_duration(3) + + video=concatenate_videoclips([red, green, blue], method="compose") + assert video.duration == 6 + assert video.mask.duration == 6 + + video=concatenate_videoclips([red, green, blue]) + assert video.duration == 6 + if __name__ == '__main__': pytest.main() +