diff --git a/CHANGELOG.md b/CHANGELOG.md index 93b4126dd..c8064e331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed rotate FX not being applied to mask images [\#1399](https://github.com/Zulko/moviepy/pull/1399) - Fixed opacity error blitting VideoClips [\#1552](https://github.com/Zulko/moviepy/pull/1552) - Fixed rotation metadata of input not being taken into account rendering VideoClips [\#577](https://github.com/Zulko/moviepy/pull/577) +- Fixed mono clips crashing when `audio_fadein` FX applied [\#1574](https://github.com/Zulko/moviepy/pull/1574) ## [v2.0.0.dev2](https://github.com/zulko/moviepy/tree/v2.0.0.dev2) (2020-10-05) diff --git a/moviepy/audio/fx/audio_fadein.py b/moviepy/audio/fx/audio_fadein.py index a0158cff5..e62c6412e 100644 --- a/moviepy/audio/fx/audio_fadein.py +++ b/moviepy/audio/fx/audio_fadein.py @@ -1,23 +1,45 @@ import numpy as np -from moviepy.decorators import audio_video_fx +from moviepy.decorators import audio_video_fx, convert_parameter_to_seconds + + +def _mono_factor_getter(): + return lambda t, duration: np.minimum(t / duration, 1) + + +def _stereo_factor_getter(nchannels): + def getter(t, duration): + factor = np.minimum(t / duration, 1) + return np.array([factor for _ in range(nchannels)]).T + + return getter @audio_video_fx +@convert_parameter_to_seconds(["duration"]) def audio_fadein(clip, duration): """Return an audio (or video) clip that is first mute, then the sound arrives progressively over ``duration`` seconds. - """ - def fading(get_frame, t): - frame = get_frame(t) + Parameters + ---------- + + duration : float + How long does it take for the sound to return to its normal level. - if np.isscalar(t): - factor = min(1.0 * t / duration, 1) - factor = np.array([factor, factor]) - else: - factor = np.minimum(1.0 * t / duration, 1) - factor = np.vstack([factor, factor]).T - return factor * frame + Examples + -------- + + >>> clip = VideoFileClip("media/chaplin.mp4") + >>> clip.fx(audio_fadein, "00:00:06") + """ + get_factor = ( + _mono_factor_getter() + if clip.nchannels == 1 + else _stereo_factor_getter(clip.nchannels) + ) - return clip.transform(fading, keep_duration=True) + return clip.transform( + lambda get_frame, t: get_factor(t, duration) * get_frame(t), + keep_duration=True, + ) diff --git a/tests/test_fx.py b/tests/test_fx.py index c06823692..3de4f55d3 100644 --- a/tests/test_fx.py +++ b/tests/test_fx.py @@ -19,10 +19,12 @@ ) from moviepy.audio.fx import ( audio_delay, + audio_fadein, audio_normalize, multiply_stereo_volume, multiply_volume, ) +from moviepy.tools import convert_to_seconds from moviepy.utils import close_all_clips from moviepy.video.fx import ( blackwhite, @@ -1279,5 +1281,66 @@ def test_audio_delay(duration, offset, n_repeats, decay): ) +@pytest.mark.parametrize("sound_type", ("stereo", "mono")) +@pytest.mark.parametrize("fps", (44100, 22050)) +@pytest.mark.parametrize( + ("clip_duration", "fadein_duration"), + ( + ( + (0.2, 0.1), + (1, "00:00:00,4"), + (0.3, 0.13), + ) + ), +) +def test_audio_fadein(sound_type, fps, clip_duration, fadein_duration): + 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=clip_duration, fps=fps) + new_clip = audio_fadein(clip, fadein_duration) + + # first frame is muted + first_frame = new_clip.get_frame(0) + if sound_type == "stereo": + assert len(first_frame) > 1 + for value in first_frame: + assert value == 0.0 + else: + assert first_frame == 0.0 + + fadein_duration = convert_to_seconds(fadein_duration) + + n_parts = 10 + + # cut transformed part into subclips and check the expected max_volume for + # each one + time_foreach_part = fadein_duration / n_parts + start_times = np.arange(0, fadein_duration, time_foreach_part) + for i, start_time in enumerate(start_times): + end_time = start_time + time_foreach_part + subclip_max_volume = new_clip.subclip(start_time, end_time).max_volume() + + possible_value = (i + 1) / n_parts + assert round(subclip_max_volume, 2) in [ + possible_value, + round(possible_value - 0.01, 5), + ] + + # cut non transformed part into subclips and check the expected max_volume + # for each one (almost 1) + time_foreach_part = (clip_duration - fadein_duration) / n_parts + start_times = np.arange(fadein_duration, clip_duration, time_foreach_part) + for i, start_time in enumerate(start_times): + end_time = start_time + time_foreach_part + subclip_max_volume = new_clip.subclip(start_time, end_time).max_volume() + + assert round(subclip_max_volume, 4) == 1 + + if __name__ == "__main__": pytest.main()