Skip to main content

ranim_core/animation/
sound.rs

1//! Audio leaf animation: a sound placed and composed like any animation.
2//!
3//! [`Sound`](crate::animation::sound::Sound) makes the audio plane part of the animation tree: a sound sits
4//! in `seq!`/`stack!`/`lagged!` beside visual animations, shares their
5//! placement vocabulary ([`Unplaced::at`](crate::animation::build::Unplaced::at), duration overrides, enable), and
6//! never enters the per-frame evaluation pipeline.
7//!
8//! The leaf contract mirrors `Eval`: content is a pure span, and all time
9//! management (windows, placement, container remaps) lives on the cells. At
10//! mix time the cells' remaps compose along the tree path — container rate
11//! functions and duration overrides warp a sound's playback exactly like the
12//! surrounding motion, pitch movement included.
13
14use std::any::type_name;
15use std::ops::Range;
16
17use crate::audio::{AudioClip, AudioTrack};
18
19use crate::animation::build::{IntoAnimNode, Unplaced};
20use crate::animation::node::{AnimNode, NodeContent};
21
22/// An audio leaf: plays a clip's span inside its placed window.
23///
24/// Authored with [`Sound::new`] plus the track shapers (`with_gain`,
25/// `with_fade_in`, ...), then composed exactly like a visual animation:
26///
27/// ```ignore
28/// seq![
29///     square.fade_in(),
30///     Sound::new(narration),          // plays while the next cells run
31///     square.write(),
32/// ]
33/// ```
34pub struct Sound {
35    track: AudioTrack,
36}
37
38impl Sound {
39    /// A sound playing the whole clip at unit gain.
40    pub fn new(clip: AudioClip) -> Self {
41        Self {
42            track: AudioTrack::new(clip),
43        }
44    }
45
46    /// A sound from a shaped track (gain, fades, play range).
47    pub fn from_track(track: AudioTrack) -> Self {
48        Self { track }
49    }
50
51    /// Set the sound's linear gain.
52    pub fn with_gain(mut self, gain: f64) -> Self {
53        self.track = self.track.with_gain(gain);
54        self
55    }
56
57    /// Fade in linearly over the first `secs` of the sound.
58    pub fn with_fade_in(mut self, secs: f64) -> Self {
59        self.track = self.track.with_fade_in(secs);
60        self
61    }
62
63    /// Fade out linearly over the last `secs` of the sound.
64    pub fn with_fade_out(mut self, secs: f64) -> Self {
65        self.track = self.track.with_fade_out(secs);
66        self
67    }
68
69    /// Play only the clip's seconds within `range` — a content trim, unlike
70    /// the cell layer's `with_duration`, which resamples the sound to fit a
71    /// new window length.
72    pub fn with_play_secs(mut self, range: Range<f64>) -> Self {
73        self.track = self.track.with_play_secs(range);
74        self
75    }
76
77    /// The sound's natural window length: the clip's play length.
78    pub fn duration_secs(&self) -> f64 {
79        self.track.play_window_secs()
80    }
81}
82
83impl Unplaced for Sound {}
84impl IntoAnimNode for Sound {
85    fn into_anim_node(self) -> AnimNode {
86        let window = self.duration_secs();
87        AnimNode {
88            content: NodeContent::Audio(Box::new(self.track)),
89            internal_time_secs: window,
90            rate_func: None,
91            time_range: 0.0..window,
92            enabled: true,
93            anim_name: type_name::<Self>(),
94        }
95    }
96}