Repository: Zulko/moviepy

Issue:
Allow to specify time range in multiply_volume effect
Currently, if you want to create a silence in a clip, you must do something like:

```python
import numpy as np

from moviepy.editor import *


def make_frame(t):  # create stereo A note
    return np.array(
        [np.sin(440 * 2 * np.pi * t), np.sin(160 * 2 * np.pi * t)]
    ).T.copy(order="C")

wave_clip = AudioClip(make_frame, duration=2, fps=44100)
silenced_clip = concatenate_audioclips([
    wave_clip.subclip(0, .5),  # half second playing
    wave_clip.subclip(.5, 1).multiply_volume(0),  # half second silence
    wave_clip.subclip(1, 2),  # one second playing
])
```

Looks crazy and is not optime. I've been looking for other solutions but I can't find nothing more. I would prefer something like this:

```python
import numpy as np

from moviepy.editor import *


def make_frame(t):  # create stereo A note
    return np.array(
        [np.sin(440 * 2 * np.pi * t), np.sin(160 * 2 * np.pi * t)]
    ).T.copy(order="C")

wave_clip = AudioClip(make_frame, duration=2, fps=44100)
silenced_clip = wave_clip.multiply_volume(0, start=.5, end=1)
```

So just adding two parameters `start` and `end` to specify a range in which the volume transform would be applied for `multiply_volume` FX seems obvious to me.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/39882716/badge)](https://coveralls.io/builds/39882716)

Coverage increased (+0.1%) to 68.737% when pulling **5c4c5a220845d6e3c6daa943b2fa239dee0fb07e on mondeja:multiply_volume-start-end** into **a472c864d21070828b49d59eb8d25be2eb7752e4 on Zulko:master**.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cf091f..21f62a4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `video.io.ffmpeg_reader.ffmpeg_parse_infos` returns metadata of the container in attribute `metadata` [\#1466](https://github.com/Zulko/moviepy/pull/1466)
 - `center`, `translate` and `bg_color` arguments to `video.fx.rotate` [\#1474](https://github.com/Zulko/moviepy/pull/1474)
 - `audio.fx.audio_delay` FX [\#1481](https://github.com/Zulko/moviepy/pull/1481)
+- `start_time` and `end_time` optional arguments to `multiply_volume` FX which allow to specify a range applying the transformation [\#1572](https://github.com/Zulko/moviepy/pull/1572)
 
 ### Changed <!-- for changes in existing functionality -->
 - Lots of method and parameter names have been changed. This will be explained better in the documentation soon. See https://github.com/Zulko/moviepy/pull/1170 for more information. [\#1170](https://github.com/Zulko/moviepy/pull/1170)
diff --git a/moviepy/audio/fx/multiply_volume.py b/moviepy/audio/fx/multiply_volume.py
index 46efc50..6d7a86b 100644
--- a/moviepy/audio/fx/multiply_volume.py
+++ b/moviepy/audio/fx/multiply_volume.py
@@ -1,22 +1,68 @@
-from moviepy.decorators import audio_video_fx
+import numpy as np
+
+from moviepy.decorators import audio_video_fx, convert_parameter_to_seconds
+
+
+def _multiply_volume_in_range(factor, start_time, end_time, nchannels):
+    def factors_filter(factor, t):
+        return np.array([factor if start_time <= t_ <= end_time else 1 for t_ in t])
+
+    def multiply_stereo_volume(get_frame, t):
+        return np.multiply(
+            get_frame(t),
+            np.array([factors_filter(factor, t) for _ in range(nchannels)]).T,
+        )
+
+    def multiply_mono_volume(get_frame, t):
+        return np.multiply(get_frame(t), factors_filter(factor, t))
+
+    return multiply_mono_volume if nchannels == 1 else multiply_stereo_volume
 
 
 @audio_video_fx
-def multiply_volume(clip, factor):
+@convert_parameter_to_seconds(["start_time", "end_time"])
+def multiply_volume(clip, factor, start_time=None, end_time=None):
     """Returns a clip with audio volume multiplied by the
     value `factor`. Can be applied to both audio and video clips.
 
-    This effect is loaded as a clip method when you use moviepy.editor,
-    so you can just write ``clip.multiply_volume(2)``
+    Parameters
+    ----------
+
+    factor : float
+      Volume multiplication factor.
+
+    start_time : float, optional
+      Time from the beginning of the clip until the volume transformation
+      begins to take effect, in seconds. By default at the beginning.
+
+    end_time : float, optional
+      Time from the beginning of the clip until the volume transformation
+      ends to take effect, in seconds. By default at the end.
 
     Examples
     --------
 
     >>> from moviepy import AudioFileClip
+    >>>
     >>> music = AudioFileClip('music.ogg')
-    >>> new_clip = clip.multiply_volume(2)  # doubles audio volume
-    >>> new_clip = clip.multiply_volume(0.5)  # half audio
+    >>> doubled_audio_clip = clip.multiply_volume(2)  # doubles audio volume
+    >>> half_audio_clip = clip.multiply_volume(0.5)  # half audio
+    >>>
+    >>> # silenced clip during one second at third
+    >>> silenced_clip = clip.multiply_volume(0, start_time=2, end_time=3)
     """
+    if start_time is None and end_time is None:
+        return clip.transform(
+            lambda get_frame, t: factor * get_frame(t),
+            keep_duration=True,
+        )
+
     return clip.transform(
-        lambda get_frame, t: factor * get_frame(t), keep_duration=True
+        _multiply_volume_in_range(
+            factor,
+            clip.start if start_time is None else start_time,
+            clip.end if end_time is None else end_time,
+            clip.nchannels,
+        ),
+        keep_duration=True,
     )


Test patch:
diff --git a/tests/test_fx.py b/tests/test_fx.py
index 6162864..6db9012 100644
--- a/tests/test_fx.py
+++ b/tests/test_fx.py
@@ -1203,48 +1203,213 @@ def test_audio_normalize_muted():
     close_all_clips(locals())
 
 
-def test_multiply_volume():
-    clip = AudioFileClip("media/crunching.mp3")
+@pytest.mark.parametrize(
+    ("sound_type", "factor", "duration", "start_time", "end_time"),
+    (
+        pytest.param(
+            "stereo",
+            0,
+            None,
+            None,
+            None,
+            id="stereo-0",
+        ),
+        pytest.param(
+            "stereo",
+            2,
+            None,
+            None,
+            None,
+            id="stereo-2",
+        ),
+        pytest.param(
+            "mono",
+            3,
+            None,
+            None,
+            None,
+            id="mono-3",
+        ),
+        pytest.param(
+            "stereo",
+            0,
+            0.2,
+            "00:00:00,1",
+            None,
+            id="stereo-0-start=.1",
+        ),
+        pytest.param(
+            "stereo",
+            0,
+            0.3,
+            None,
+            (0, 0, 0.2),
+            id="stereo-0-end=.2",
+        ),
+        pytest.param(
+            "stereo",
+            0,
+            0.3,
+            0.1,
+            0.2,
+            id="stereo-0-start=.1-end=.2",
+        ),
+        pytest.param(
+            "mono",
+            0,
+            0.3,
+            0.2,
+            None,
+            id="mono-0-start=.2",
+        ),
+        pytest.param(
+            "mono",
+            0,
+            0.2,
+            None,
+            "00:00:00.1",
+            id="mono-0-end=.1",
+        ),
+        pytest.param(
+            "mono",
+            2,
+            0.3,
+            0.1,
+            0.2,
+            id="mono-0-start=.1-end=.2",
+        ),
+    ),
+)
+def test_multiply_volume_audioclip(
+    sound_type,
+    factor,
+    duration,
+    start_time,
+    end_time,
+):
+    if sound_type == "stereo":
+        make_frame = lambda t: np.array(
+            [
+                np.sin(440 * 2 * np.pi * t),
+                np.sin(160 * 2 * np.pi * t),
+            ]
+        ).T.copy(order="C")
+    else:
+        make_frame = lambda t: [np.sin(440 * 2 * np.pi * t)]
+
+    clip = AudioClip(
+        make_frame,
+        duration=duration if duration else 0.1,
+        fps=22050,
+    )
     clip_array = clip.to_soundarray()
 
-    # stereo mute
-    clip_muted = multiply_volume(clip, 0)
+    clip_transformed = multiply_volume(
+        clip,
+        factor,
+        start_time=start_time,
+        end_time=end_time,
+    )
+    clip_transformed_array = clip_transformed.to_soundarray()
 
-    left_channel_muted = clip_muted.to_soundarray()[:, 0]
-    right_channel_muted = clip_muted.to_soundarray()[:, 1]
+    assert len(clip_transformed_array)
 
-    z_channel = np.zeros(len(left_channel_muted))
+    if hasattr(clip_array, "shape") and len(clip_array.shape) > 1:
+        # stereo clip
+        left_channel_transformed = clip_transformed_array[:, 0]
+        right_channel_transformed = clip_transformed_array[:, 1]
 
-    assert np.array_equal(left_channel_muted, z_channel)
-    assert np.array_equal(right_channel_muted, z_channel)
+        if start_time is None and end_time is None:
+            expected_left_channel_transformed = clip_array[:, 0] * factor
+            expected_right_channel_transformed = clip_array[:, 1] * factor
+        else:
+            start_time = convert_to_seconds(start_time) if start_time else clip.start
+            end_time = convert_to_seconds(end_time) if end_time else clip.end
+
+            expected_left_channel_transformed = np.array([])
+            expected_right_channel_transformed = np.array([])
+            for i, frame in enumerate(clip_array):
+                t = i / clip.fps
+                transformed_frame = frame * (
+                    factor if start_time <= t <= end_time else 1
+                )
+                expected_left_channel_transformed = np.append(
+                    expected_left_channel_transformed,
+                    transformed_frame[0],
+                )
+                expected_right_channel_transformed = np.append(
+                    expected_right_channel_transformed,
+                    transformed_frame[1],
+                )
 
-    # stereo level doubled
-    clip_doubled = multiply_volume(clip, 2)
-    clip_doubled_array = clip_doubled.to_soundarray()
-    left_channel_doubled = clip_doubled_array[:, 0]
-    right_channel_doubled = clip_doubled_array[:, 1]
-    expected_left_channel_doubled = clip_array[:, 0] * 2
-    expected_right_channel_doubled = clip_array[:, 1] * 2
+        assert len(left_channel_transformed)
+        assert len(expected_left_channel_transformed)
+        assert np.array_equal(
+            left_channel_transformed,
+            expected_left_channel_transformed,
+        )
 
-    assert np.array_equal(left_channel_doubled, expected_left_channel_doubled)
-    assert np.array_equal(right_channel_doubled, expected_right_channel_doubled)
+        assert len(right_channel_transformed)
+        assert len(expected_right_channel_transformed)
+        assert np.array_equal(
+            right_channel_transformed,
+            expected_right_channel_transformed,
+        )
 
-    # mono muted
-    sinus_wave = lambda t: [np.sin(440 * 2 * np.pi * t)]
-    mono_clip = AudioClip(sinus_wave, duration=1, fps=22050)
-    muted_mono_clip = multiply_volume(mono_clip, 0)
-    mono_channel_muted = muted_mono_clip.to_soundarray()
+    else:
+        # mono clip
 
-    z_channel = np.zeros(len(mono_channel_muted))
-    assert np.array_equal(mono_channel_muted, z_channel)
+        if start_time is None and end_time is None:
+            expected_clip_transformed_array = clip_array * factor
+        else:
+            start_time = convert_to_seconds(start_time) if start_time else clip.start
+            end_time = convert_to_seconds(end_time) if end_time else clip.end
+
+            expected_clip_transformed_array = np.array([])
+            for i, frame in enumerate(clip_array[0]):
+                t = i / clip.fps
+                transformed_frame = frame * (
+                    factor if start_time <= t <= end_time else 1
+                )
+                expected_clip_transformed_array = np.append(
+                    expected_clip_transformed_array,
+                    transformed_frame,
+                )
+            expected_clip_transformed_array = np.array(
+                [
+                    expected_clip_transformed_array,
+                ]
+            )
 
-    mono_clip = AudioClip(sinus_wave, duration=1, fps=22050)
-    doubled_mono_clip = multiply_volume(mono_clip, 2)
-    mono_channel_doubled = doubled_mono_clip.to_soundarray()
-    d_channel = mono_clip.to_soundarray() * 2
-    assert np.array_equal(mono_channel_doubled, d_channel)
+        assert len(expected_clip_transformed_array)
 
-    close_all_clips(locals())
+        assert np.array_equal(
+            expected_clip_transformed_array,
+            clip_transformed_array,
+        )
+
+
+def test_multiply_volume_videoclip():
+    start_time, end_time = (0.1, 0.2)
+
+    clip = multiply_volume(
+        VideoFileClip("media/chaplin.mp4").subclip(0, 0.3),
+        0,
+        start_time=start_time,
+        end_time=end_time,
+    )
+    clip_soundarray = clip.audio.to_soundarray()
+
+    assert len(clip_soundarray)
+
+    expected_silence = np.zeros(clip_soundarray.shape[1])
+
+    for i, frame in enumerate(clip_soundarray):
+        t = i / clip.audio.fps
+        if start_time <= t <= end_time:
+            assert np.array_equal(frame, expected_silence)
+        else:
+            assert not np.array_equal(frame, expected_silence)
 
 
 def test_multiply_stereo_volume():