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}