Skip to content

Sound

The film's code
import manimgx as m
import numpy as np


class SoundHero(m.Scene):
    def construct(self) -> None:
        def tone(t: np.ndarray) -> np.ndarray:
            return 0.3 * np.sin(2 * np.pi * 440 * t) * np.exp(-3 * t)

        dots = m.VGroup(
            *[m.Dot(radius=0.25, color=m.YELLOW) for _ in range(4)]
        ).arrange(buff=1)
        for dot in dots:
            self.play(
                m.FadeIn(dot, scale=2), m.Sound.of(tone, duration=0.5), run_time=0.5
            )
        self.wait()

A sound is read from a file (WAV, MP3, AAC, FLAC and the other usual kinds), or made by a function of time. manimgx places each sound at its moment in the video, and mixes them into one sound track.

Give a sound to play with animations, and it starts with them. To start one at any moment, add it: the scene goes on while it plays. A sound plays to its end, or until you stop the clip that adding it gives back.

A sound's methods make a changed sound: quieter or louder, faded in or out, trimmed, looped, faster, panned, or ducked under a voice.

Sound

A sound a film plays.

Make one from a file, the bytes of one, or samples; edit it (each edit makes a new sound); place it in a scene: add_sound starts it now and takes no time, while play plays it as a part of the play, which then lasts at least as long as the sound. In a composition it starts when its window opens: LaggedStart(*(AnimationGroup(FadeIn(d), click) for d in dots)) clicks as each dot appears. It changes nothing on screen, and a play's run_time does not change it: a sound lasts its duration.

m.Sound(source, *, rate=None)
source

A file's path, a file's bytes, or samples: floats from -1 to 1, one column per channel (a 1-D array is mono), copied as they are given.

rate

The samples' rate, in samples a second; only for samples.

Source

src/manimgx/audio/sound.py

def __init__(self, source: Source, *, rate: int | None = None) -> None:
    if isinstance(source, np.ndarray) != (rate is not None):
        raise ValueError("give a rate with samples, and only with samples")
    if isinstance(source, str | os.PathLike) and not Path(source).is_file():
        raise FileNotFoundError(f"no sound file at {os.fspath(source)!r}")
    if isinstance(source, np.ndarray):  # its own: the samples as they were given
        source = np.array(source, copy=True, subok=True)
    self.source = source
    self.rate = rate
    self._edit = _Edit()
    super().__init__(None)

samples

Its samples at RATE: floats, one column per channel (1 or 2).

sound.samples
Source

src/manimgx/audio/sound.py

def samples(self) -> np.ndarray:
    """Its samples at `RATE`: floats, one column per channel (1 or 2)."""
    made = self.__dict__.get("_made")
    if made is None:
        made = self.__dict__["_made"] = _make(self)
    return made

duration

How long it lasts, in seconds (inf for a sound looped until stopped).

sound.duration
Source

src/manimgx/audio/sound.py

def duration(self) -> float:
    """How long it lasts, in seconds (inf for a sound looped until stopped)."""
    if self._edit.loop is not None:
        return self._edit.loop
    return len(self.samples) / RATE

run_time

Its duration: a sound plays for as long as it lasts. An endless one (loop()) cannot be played: add it (add_sound), and stop it.

sound.run_time
Source

src/manimgx/audio/sound.py

def run_time(self) -> float:
    """Its duration: a sound plays for as long as it lasts. An endless one (`loop()`)
    cannot be played: add it ([`add_sound`][manimgx.Scene.add_sound]), and stop it.
    """
    if self._edit.loop == math.inf:
        raise ValueError(
            "an endless sound (loop()) never ends: add it with add_sound, and stop"
            " its clip, instead of playing it"
        )
    return self.duration

gain

This sound louder by decibels (quieter if negative: -6 halves it).

sound.gain(decibels)
Source

src/manimgx/audio/sound.py

def gain(self, decibels: float) -> Self:
    """This sound louder by `decibels` (quieter if negative: -6 halves it)."""
    return self._but(gain=self._edit.gain + decibels)

fade_in

This sound fading in from silence over its first seconds.

sound.fade_in(seconds)
Source

src/manimgx/audio/sound.py

def fade_in(self, seconds: float) -> Self:
    """This sound fading in from silence over its first `seconds`."""
    return self._but(fade_in=seconds)

fade_out

This sound fading out to silence over its last seconds.

sound.fade_out(seconds)
Source

src/manimgx/audio/sound.py

def fade_out(self, seconds: float) -> Self:
    """This sound fading out to silence over its last `seconds`."""
    return self._but(fade_out=seconds)

trim

The part of this sound from start to end seconds (None: its end).

sound.trim(start=0.0, end=None)
Source

src/manimgx/audio/sound.py

def trim(self, start: float = 0.0, end: float | None = None) -> Self:
    """The part of this sound from `start` to `end` seconds (None: its end)."""
    return self._but(start=start, end=end)

loop

This sound repeated to last duration seconds; None: until the film ends, or until its clip is stopped.

sound.loop(duration=None)
Source

src/manimgx/audio/sound.py

def loop(self, duration: float | None = None) -> Self:
    """This sound repeated to last `duration` seconds; None: until the film ends, or
    until its clip is [stopped][manimgx.audio.sound.Clip.stop]."""
    return self._but(loop=math.inf if duration is None else duration)

speed

This sound played factor times as fast, as a tape is: higher and shorter.

sound.speed(factor)
Source

src/manimgx/audio/sound.py

def speed(self, factor: float) -> Self:
    """This sound played `factor` times as fast, as a tape is: higher and shorter."""
    return self._but(speed=self._edit.speed * factor)

pan

This sound placed from -1 (left) through 0 (center) to 1 (right).

sound.pan(position)
Source

src/manimgx/audio/sound.py

def pan(self, position: float) -> Self:
    """This sound placed from -1 (left) through 0 (center) to 1 (right)."""
    return self._but(pan=position)

duck

This sound dipping by decibels while anything speaks (while a Speech plays): music under a narration. It eases down DUCK_ATTACK seconds before the speech and back up over DUCK_RELEASE after.

sound.duck(decibels=12)
Source

src/manimgx/audio/sound.py

def duck(self, decibels: float = 12) -> Self:
    """This sound dipping by `decibels` while anything speaks (while a
    [`Speech`][manimgx.audio.Speech] plays): music under a narration. It eases down
    `DUCK_ATTACK` seconds before the speech and back up over `DUCK_RELEASE` after.
    """
    return self._but(duck=decibels)

of

Sound.of(lambda t: 0.3 * np.sin(2 * np.pi * 440 * t), 1): a second of A.

A sound made by a function of time: function(t), given the times of its samples (seconds, an array), returns their values (-1 to 1).

Sound.of(function, duration)
Source

src/manimgx/audio/sound.py

@classmethod
def of(
    cls, function: Callable[[np.ndarray], np.ndarray], duration: float
) -> "Sound":
    """A sound made by a function of time: `function(t)`, given the times of its
    samples (seconds, an array), returns their values (-1 to 1).

    Examples:
        `Sound.of(lambda t: 0.3 * np.sin(2 * np.pi * 440 * t), 1)`: a second of A.
    """
    t = np.arange(round(duration * RATE)) / RATE
    return cls(np.asarray(function(t), np.float32), rate=RATE)

add_sound

Start a sound: now, or time_offset seconds from now; it takes no time.

The scene goes on at once, and the sound plays over whatever follows, until it ends (or the film does, or its clip is stopped). Called by an updater, it starts at the instant the updater runs for: a bounce sounds exactly at the bounce. To wait for a sound, play it instead.

self.add_sound(sound, time_offset=0, gain=None)
sound

The sound, or its file.

time_offset

How long after now it starts, in seconds.

gain

How much louder it plays, in decibels (negative: quieter).

Returns stop it to end it early.

Source

src/manimgx/scene.py

def add_sound(
    self,
    sound: str | os.PathLike[str] | Sound,
    time_offset: float = 0,
    gain: float | None = None,
) -> Clip:
    """Start a sound: now, or `time_offset` seconds from now; it takes no time.

    The scene goes on at once, and the sound plays over whatever follows, until it
    ends (or the film does, or its clip is stopped). Called by an updater, it starts
    at the instant the updater runs for: a bounce sounds exactly at the bounce. To
    wait for a sound, [`play`][manimgx.Scene.play] it instead.

    Args:
        sound: The sound, or its file.
        time_offset: How long after now it starts, in seconds.
        gain: How much louder it plays, in decibels (negative: quieter).

    Returns:
        Its clip: [`stop`][manimgx.audio.sound.Clip.stop] it to end it early.
    """
    from manimgx.audio.sound import Clip, Sound

    if not isinstance(sound, Sound):
        sound = Sound(sound)
    if gain:
        sound = sound.gain(gain)
    # decoded now: a file it cannot read fails here, not at the end of the render
    _ = sound.samples
    clip = Clip(sound, clock.now + _exact(time_offset))
    self.film.clips.append(clip)
    return clip

Clip

A sound as a film plays it: from start, in seconds of scene time, until it ends or is stopped. add_sound returns it.

stop

Stop the sound: it fades out from now (the instant the scene is computing) over fade seconds, and is silent after.

clip.stop(fade=0.05)
Source

src/manimgx/audio/sound.py

def stop(self, fade: float = 0.05) -> None:
    """Stop the sound: it fades out from now (the instant the scene is computing) over
    `fade` seconds, and is silent after."""
    self.end, self.fade = clock.now + Fraction(fade).limit_denominator(RATE), fade