mirror of
https://github.com/3b1b/manim.git
synced 2025-11-14 04:57:46 +00:00
Added rudimentary sound abilities to scene
This commit is contained in:
parent
6652dc6006
commit
7e0f30614a
2 changed files with 87 additions and 15 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue