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()
+