Added rudimentary sound abilities to scene

This commit is contained in:
Grant Sanderson 2019-01-16 11:10:43 -08:00
parent 6652dc6006
commit 7e0f30614a
2 changed files with 87 additions and 15 deletions

View file

@ -10,6 +10,7 @@ import warnings
from tqdm import tqdm as ProgressDisplay from tqdm import tqdm as ProgressDisplay
import numpy as np import numpy as np
from pydub import AudioSegment
from manimlib.animation.animation import Animation from manimlib.animation.animation import Animation
from manimlib.animation.creation import Write from manimlib.animation.creation import Write
@ -47,6 +48,7 @@ class Scene(Container):
"to_twitch": False, "to_twitch": False,
"twitch_key": None, "twitch_key": None,
"output_file_name": None, "output_file_name": None,
"leave_progress_bars": False,
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
@ -65,17 +67,20 @@ class Scene(Container):
random.seed(self.random_seed) random.seed(self.random_seed)
np.random.seed(self.random_seed) np.random.seed(self.random_seed)
self.init_audio()
self.setup() self.setup()
if self.livestreaming: if self.livestreaming:
return None return None
try: try:
self.construct(*self.construct_args) self.construct(*self.construct_args)
except EndSceneEarlyException: except EndSceneEarlyException:
pass if hasattr(self, "writing_process"):
self.writing_process.terminate()
self.tear_down() self.tear_down()
if self.write_to_movie: if self.write_to_movie:
self.combine_movie_files() self.combine_movie_files()
self.print_end_message()
def handle_play_like_call(func): def handle_play_like_call(func):
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
@ -116,6 +121,9 @@ class Scene(Container):
return self.output_file_name return self.output_file_name
return str(self) return str(self)
def print_end_message(self):
print("Played {} animations".format(self.num_plays))
def set_variables_as_attrs(self, *objects, **newly_named_objects): def set_variables_as_attrs(self, *objects, **newly_named_objects):
""" """
This method is slightly hacky, making it a little easier This method is slightly hacky, making it a little easier
@ -134,6 +142,38 @@ class Scene(Container):
def get_attrs(self, *keys): def get_attrs(self, *keys):
return [getattr(self, key) for key in keys] return [getattr(self, key) for key in keys]
# Sound
def init_audio(self):
self.includes_sound = False
def create_audio_segment(self):
self.audio_segment = AudioSegment.silent()
def add_audio_segment(self, new_segment, time_offset=0):
if not self.includes_sound:
self.includes_sound = True
self.create_audio_segment()
segment = self.audio_segment
overly_time = self.get_time() + time_offset
if overly_time < 0:
raise Exception("Adding sound at timestamp < 0")
curr_end = segment.duration_seconds
new_end = overly_time + new_segment.duration_seconds
diff = new_end - curr_end
if diff > 0:
segment = segment.append(
AudioSegment.silent(int(np.ceil(diff * 1000))),
crossfade=0,
)
self.audio_segment = segment.overlay(
new_segment, position=int(1000 * overly_time)
)
def add_sound(self, sound_file, time_offset=0):
new_segment = AudioSegment.from_file(sound_file)
self.add_audio_segment(new_segment, 0)
# Only these methods should touch the camera # Only these methods should touch the camera
def set_camera(self, camera): def set_camera(self, camera):
@ -393,12 +433,15 @@ class Scene(Container):
times = np.arange(0, run_time, step) times = np.arange(0, run_time, step)
time_progression = ProgressDisplay( time_progression = ProgressDisplay(
times, total=n_iterations, times, total=n_iterations,
leave=False, leave=self.leave_progress_bars,
) )
return time_progression return time_progression
def get_run_time(self, animations):
return np.max([animation.run_time for animation in animations])
def get_animation_time_progression(self, animations): def get_animation_time_progression(self, animations):
run_time = np.max([animation.run_time for animation in animations]) run_time = self.get_run_time(animations)
time_progression = self.get_time_progression(run_time) time_progression = self.get_time_progression(run_time)
time_progression.set_description("".join([ time_progression.set_description("".join([
"Animation {}: ".format(self.num_plays), "Animation {}: ".format(self.num_plays),
@ -500,20 +543,18 @@ class Scene(Container):
# have to be rendered every frame # have to be rendered every frame
self.update_frame(excluded_mobjects=moving_mobjects) self.update_frame(excluded_mobjects=moving_mobjects)
static_image = self.get_frame() static_image = self.get_frame()
total_run_time = 0
for t in self.get_animation_time_progression(animations): for t in self.get_animation_time_progression(animations):
for animation in animations: for animation in animations:
animation.update(t / animation.run_time) animation.update(t / animation.run_time)
self.continual_update(dt=t - total_run_time) self.continual_update(dt=self.frame_duration)
self.update_frame(moving_mobjects, static_image) self.update_frame(moving_mobjects, static_image)
self.add_frames(self.get_frame()) self.add_frames(self.get_frame())
total_run_time = t
self.mobjects_from_last_animation = [ self.mobjects_from_last_animation = [
anim.mobject for anim in animations anim.mobject for anim in animations
] ]
self.clean_up_animations(*animations) self.clean_up_animations(*animations)
if self.skip_animations: if self.skip_animations:
self.continual_update(total_run_time) self.continual_update(self.get_run_time(animations))
else: else:
self.continual_update(0) self.continual_update(0)
@ -565,16 +606,14 @@ class Scene(Container):
@handle_play_like_call @handle_play_like_call
def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None):
if self.should_continually_update(): if self.should_continually_update():
total_time = 0
time_progression = self.get_wait_time_progression(duration, stop_condition) time_progression = self.get_wait_time_progression(duration, stop_condition)
for t in time_progression: for t in time_progression:
self.continual_update(dt=t - total_time) self.continual_update(dt=self.frame_duration)
self.update_frame() self.update_frame()
self.add_frames(self.get_frame()) self.add_frames(self.get_frame())
if stop_condition and stop_condition(): if stop_condition and stop_condition():
time_progression.clear() time_progression.close()
break break
total_time = t
elif self.skip_animations: elif self.skip_animations:
# Do nothing # Do nothing
return self return self
@ -663,7 +702,9 @@ class Scene(Container):
) )
) )
temp_file_path = file_path.replace(".", "_temp.") temp_file_path = file_path.replace(".", "_temp.")
self.args_to_rename_file = (temp_file_path, file_path)
self.movie_file_path = file_path
self.temp_movie_file_path = temp_file_path
fps = int(1 / self.frame_duration) fps = int(1 / self.frame_duration)
height = self.camera.get_pixel_height() height = self.camera.get_pixel_height()
@ -709,9 +750,13 @@ class Scene(Container):
self.writing_process.wait() self.writing_process.wait()
if self.livestreaming: if self.livestreaming:
return True return True
shutil.move(*self.args_to_rename_file) shutil.move(
self.temp_movie_file_path,
self.movie_file_path,
)
def combine_movie_files(self): def combine_movie_files(self):
# TODO, this could probably use a refactor
partial_movie_file_directory = self.get_partial_movie_directory() partial_movie_file_directory = self.get_partial_movie_directory()
kwargs = { kwargs = {
"remove_non_integer_files": True, "remove_non_integer_files": True,
@ -745,12 +790,38 @@ class Scene(Container):
'-safe', '0', '-safe', '0',
'-i', file_list, '-i', file_list,
'-c', 'copy', '-c', 'copy',
'-an', # Tells FFMPEG not to expect any audio
'-loglevel', 'error', '-loglevel', 'error',
movie_file_path movie_file_path
] ]
if not self.includes_sound:
commands.insert(-1, '-an')
subprocess.call(commands) subprocess.call(commands)
os.remove(file_list) os.remove(file_list)
if self.includes_sound:
sound_file_path = movie_file_path.replace(
self.movie_file_extension, ".wav"
)
# Makes sure sound file length will match video file
self.add_audio_segment(AudioSegment.silent(0))
self.audio_segment.export(sound_file_path)
temp_file_path = movie_file_path.replace(".", "_temp.")
commands = commands = [
"ffmpeg",
"-i", movie_file_path,
"-i", sound_file_path,
'-y', # overwrite output file if it exists
"-c:v", "copy", "-c:a", "aac",
'-loglevel', 'error',
"-shortest",
"-strict", "experimental",
temp_file_path,
]
subprocess.call(commands)
shutil.move(temp_file_path, movie_file_path)
# subprocess.call(["rm", self.temp_movie_file_path])
subprocess.call(["rm", sound_file_path])
print("\nAnimation ready at {}\n".format(movie_file_path)) print("\nAnimation ready at {}\n".format(movie_file_path))
# TODO, this doesn't belong in Scene, but should be # TODO, this doesn't belong in Scene, but should be

View file

@ -7,3 +7,4 @@ scipy==1.1.0
tqdm==4.24.0 tqdm==4.24.0
opencv-python==3.4.2.17 opencv-python==3.4.2.17
pycairo==1.17.1 pycairo==1.17.1
pydub==0.23.0