Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add 'audio_delay' FX #1481

Merged
merged 8 commits into from
Jan 25, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions moviepy/audio/fx/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# import every video fx function

from moviepy.audio.fx.audio_delay import audio_delay
from moviepy.audio.fx.audio_fadein import audio_fadein
from moviepy.audio.fx.audio_fadeout import audio_fadeout
from moviepy.audio.fx.audio_loop import audio_loop
Expand All @@ -8,6 +9,7 @@
from moviepy.audio.fx.multiply_volume import multiply_volume

__all__ = (
"audio_delay",
"audio_fadein",
"audio_fadeout",
"audio_left_right",
Expand Down
55 changes: 55 additions & 0 deletions moviepy/audio/fx/audio_delay.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import numpy as np

from moviepy.decorators import audio_video_fx
from moviepy.audio.fx.multiply_volume import multiply_volume
from moviepy.audio.AudioClip import CompositeAudioClip


@audio_video_fx
def audio_delay(clip, offset=0.2, n_repeats=8, decay=0.2):
"""Return an audio (or video) clip whose audio is repeated certain number
of times with gaps between with a specific decay in its volume level.

Parameters
----------

offset (float)
Gap between repetitions start times, in seconds.

n_repeats (int)
Number of repetitions (without including the clip itself).

decay (float)
Multiplication factor for the volume level of the last repetition. Each
repetition will have a value in the linear function between 1 and this value,
increasing or decreasing constantly. Keep in mind that the last repetition
will be muted if this is 0, and if is greater than 1, the volume will increase
for each repetition.

Examples
========
mondeja marked this conversation as resolved.
Show resolved Hide resolved

>>> from moviepy import *
>>> videoclip = AudioFileClip('myaudio.wav').fx(
... audio_delay, offset=.2, n_repeats=10, decayment=.2
... )

>>> # stereo A note
>>> make_frame = lambda t: np.array(
... [np.sin(440 * 2 * np.pi * t), np.sin(880 * 2 * np.pi * t)]
... ).T
... clip = AudioClip(make_frame=make_frame, duration=0.1, fps=44100)
... clip = audio_delay(clip, offset=.2, n_repeats=11, decay=0)
"""
decayments = np.linspace(1, max(0, decay), n_repeats + 1)
return CompositeAudioClip(
[
clip.copy(),
*[
multiply_volume(
clip.with_start((rep + 1) * offset), decayments[rep + 1]
)
for rep in range(n_repeats)
],
]
)
89 changes: 88 additions & 1 deletion tests/test_fx.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

from moviepy import AudioClip, AudioFileClip, BitmapClip, ColorClip, VideoFileClip
from moviepy.audio.fx import audio_normalize
from moviepy.audio.fx import multiply_stereo_volume
from moviepy.audio.fx import (
multiply_volume,
multiply_stereo_volume,
)
from moviepy.utils import close_all_clips
from moviepy.video.fx import (
blackwhite,
Expand Down Expand Up @@ -679,5 +682,89 @@ def test_multiply_stereo_volume():
close_all_clips(locals())


@pytest.mark.parametrize(
("duration", "offset", "n_repeats", "decay"),
(
(0.1, 0.2, 11, 0),
(0.4, 2, 5, 2),
(0.5, 0.6, 3, -1),
(0.3, 1, 7, 4),
),
)
def test_audio_delay(duration, offset, n_repeats, decay):
"""Check that creating a short pulse of audio, the delay converts to a sound
with the volume level in the form `-_-_-_-_-`, being `-` pulses expressed by
`duration` argument and `_` being chunks of muted audio. Keep in mind that this
way of test the FX only works if `duration <= offset`, but as does not make sense
create a delay with `duration > offset`, this is enough for our purposes.

Note that decayment values are not tested here, but are created using
`multiply_volume`, should be OK.
"""
# limits of this test
assert n_repeats > 0 # some repetition, if not does not make sense
assert duration <= offset # avoid wave distorsion
assert not offset * 1000000 % 2 # odd offset -> no accurate muted chunk size

# stereo A note
make_frame = lambda t: np.array(
[np.sin(440 * 2 * np.pi * t), np.sin(880 * 2 * np.pi * t)]
).T.copy(order="C")

# stereo audio clip
clip = AudioClip(make_frame=make_frame, duration=duration, fps=44100)
clip_array = clip.to_soundarray()

# stereo delayed clip
delayed_clip = clip.audio_delay(offset=offset, n_repeats=n_repeats, decay=decay)
delayed_clip_array = delayed_clip.to_soundarray()

# size of chunks with audios
sound_chunk_size = clip_array.shape[0]
# muted chunks size
muted_chunk_size = int(sound_chunk_size * offset / duration) - sound_chunk_size

zeros_expected_chunk_as_muted = np.zeros((muted_chunk_size, 2))

decayments = np.linspace(1, max(0, decay), n_repeats)

for i in range(n_repeats + 1): # first clip, is not part of the repeated ones

if i == n_repeats:
# the delay ends in sound, so last muted chunk does not exists
break

# sound chunk
sound_start_at = i * sound_chunk_size + i * muted_chunk_size
sound_ends_at = sound_start_at + sound_chunk_size

# first sound chunk
if i == 0:
assert np.array_equal(
delayed_clip_array[:, :][sound_start_at:sound_ends_at],
multiply_volume(clip, decayments[i]).to_soundarray(),
)

# muted chunk
mute_starts_at = sound_ends_at + 1
mute_ends_at = mute_starts_at + muted_chunk_size

assert np.array_equal(
delayed_clip_array[:, :][mute_starts_at:mute_ends_at],
zeros_expected_chunk_as_muted,
)

# check muted bounds
assert not np.array_equal(
delayed_clip_array[:, :][mute_starts_at - 1 : mute_ends_at],
zeros_expected_chunk_as_muted,
)

assert not np.array_equal(
delayed_clip_array[:, :][mute_starts_at : mute_ends_at + 1],
zeros_expected_chunk_as_muted,
)


if __name__ == "__main__":
pytest.main()