From bf37e3cc0fdba54e2c6afaeeec16fbdd2dcec3a8 Mon Sep 17 00:00:00 2001 From: Taksh Date: Wed, 8 Jul 2026 17:04:25 +0530 Subject: [PATCH] fix(scene): record audio after cached animation replays Reset skip_animations when a play() only skipped frame rendering due to cache hits, so add_sound() is not dropped between cached waits. Fixes #4751 Co-authored-by: Cursor --- manim/renderer/cairo_renderer.py | 5 +++++ manim/utils/caching.py | 5 +++++ tests/module/scene/test_sound.py | 29 ++++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index bcbe3a4fc7..7969dc4dda 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -71,6 +71,7 @@ def play( # Needed when rendering only some animations, and skipping others. self.skip_animations = self._original_skipping_status self.update_skipping_status() + skip_due_to_cache = False scene.compile_animation_data(*args, **kwargs) @@ -96,6 +97,7 @@ def play( {"hash_current_animation": hash_current_animation}, ) self.skip_animations = True + skip_due_to_cache = True self.time += scene.duration # adding None as a partial movie file will make file_writer ignore the latter. self.file_writer.add_partial_movie_file(hash_current_animation) @@ -121,6 +123,9 @@ def play( self.file_writer.end_animation(not self.skip_animations) self.num_plays += 1 + if skip_due_to_cache: + self.skip_animations = self._original_skipping_status + self.update_skipping_status() def update_frame( # TODO Description in Docstring self, diff --git a/manim/utils/caching.py b/manim/utils/caching.py index 9ab7e6bfd0..3833eaf648 100644 --- a/manim/utils/caching.py +++ b/manim/utils/caching.py @@ -36,6 +36,7 @@ def handle_caching_play(func: Callable[..., None]) -> Callable[..., None]: def wrapper(self: OpenGLRenderer, scene: Scene, *args: Any, **kwargs: Any) -> None: self.skip_animations = self._original_skipping_status self.update_skipping_status() + skip_due_to_cache = False animations = scene.compile_animations(*args, **kwargs) scene.add_mobjects_from_animations(animations) if self.skip_animations: @@ -60,6 +61,7 @@ def wrapper(self: OpenGLRenderer, scene: Scene, *args: Any, **kwargs: Any) -> No {"hash_play": hash_play}, ) self.skip_animations = True + skip_due_to_cache = True else: hash_play = f"uncached_{self.num_plays:05}" self.animations_hashes.append(hash_play) @@ -69,5 +71,8 @@ def wrapper(self: OpenGLRenderer, scene: Scene, *args: Any, **kwargs: Any) -> No {"h": str(self.animations_hashes[:5])}, ) func(self, scene, *args, **kwargs) + if skip_due_to_cache: + self.skip_animations = self._original_skipping_status + self.update_skipping_status() return wrapper diff --git a/tests/module/scene/test_sound.py b/tests/module/scene/test_sound.py index cfa9a4da42..5b180eef7c 100644 --- a/tests/module/scene/test_sound.py +++ b/tests/module/scene/test_sound.py @@ -4,7 +4,7 @@ import wave from pathlib import Path -from manim import Scene +from manim import Scene, tempconfig def test_add_sound(tmpdir): @@ -19,3 +19,30 @@ def test_add_sound(tmpdir): scene = Scene() scene.add_sound(sound_loc) + + +def test_add_sound_after_cached_wait(tmpdir): + """Regression test for https://github.com/ManimCommunity/manim/issues/4751.""" + sound_loc = Path(tmpdir, "noise.wav") + with wave.open(str(sound_loc), "w") as f: + f.setparams((1, 2, 44100, 0, "NONE", "not compressed")) + for _ in range(4410): + f.writeframes(struct.pack("h", 1000)) + + sounds_recorded: list[int] = [] + + class AudioAfterSilenceScene(Scene): + def construct(self): + self.add_sound(str(sound_loc)) + self.wait(0.2) + self.wait(0.2) + self.add_sound(str(sound_loc)) + sounds_recorded.append(len(self.renderer.file_writer.audio_segment)) + + with tempconfig({"media_dir": str(tmpdir), "disable_caching": False}): + AudioAfterSilenceScene().render() + AudioAfterSilenceScene().render() + + assert len(sounds_recorded) == 2 + assert sounds_recorded[0] > 0 + assert sounds_recorded[1] == sounds_recorded[0]