Skip to main content

g2g_core/
controller.rs

1//! Animated properties (M882), the `gst-controller` analog: a property's value
2//! becomes a function of stream time instead of a constant set once at build
3//! time. A [`ControlSource`] is a keyframed curve, a [`ControlProgram`] binds
4//! curves to one node's property names, and the runner samples the bindings at
5//! each frame's PTS before handing that frame to the element.
6//!
7//! The program is checked against the element's own
8//! [`PropertySpec`] table when the graph starts ([`ControlProgram::resolve`]), so
9//! a misspelled or non-animatable property fails the run before any frame flows
10//! rather than animating nothing.
11
12use alloc::string::String;
13use alloc::vec::Vec;
14
15use crate::property::{PropError, PropKind, PropValue, PropertySpec};
16
17/// The log category a controller fault is reported on, so
18/// `G2G_DEBUG=controller:debug` follows the animation independently of element
19/// logging (as [`CAPS_CATEGORY`](crate::log::CAPS_CATEGORY) does for the solver).
20pub const CONTROL_CATEGORY: &str = "controller";
21
22/// A keyframed value curve, sampled by PTS. Values are `f64` regardless of the
23/// property's kind; the conversion to the property's type happens at
24/// [`apply`](ArmController::apply).
25///
26/// Both variants clamp outside their keyframe range: before the first keyframe
27/// the first value holds, after the last one the last value holds.
28// Closed set: two interpolations cover the animation cases that exist. A cubic /
29// LFO source is a real addition when something needs one, not a placeholder.
30#[derive(Debug, Clone, PartialEq)]
31pub enum ControlSource {
32    /// Hold each keyframe's value until the next keyframe's time (a discrete
33    /// knob: a mode switch, a boolean).
34    Step(Vec<(u64, f64)>),
35    /// Interpolate linearly between the surrounding keyframes (a smooth pan,
36    /// fade, or zoom).
37    Linear(Vec<(u64, f64)>),
38}
39
40impl ControlSource {
41    /// A step curve over `keys` (`(pts_ns, value)`), sorted here so the caller
42    /// need not supply them in order.
43    pub fn step(keys: impl IntoIterator<Item = (u64, f64)>) -> Self {
44        ControlSource::Step(sorted(keys))
45    }
46
47    /// A linear curve over `keys` (`(pts_ns, value)`), sorted here.
48    pub fn linear(keys: impl IntoIterator<Item = (u64, f64)>) -> Self {
49        ControlSource::Linear(sorted(keys))
50    }
51
52    fn keys(&self) -> &[(u64, f64)] {
53        match self {
54            ControlSource::Step(k) | ControlSource::Linear(k) => k,
55        }
56    }
57
58    /// The curve's value at `t_ns`. Clamps to the end values outside the
59    /// keyframe range; `0.0` for a curve with no keyframes, which
60    /// [`ControlProgram::resolve`] rejects before a run can sample it.
61    pub fn value_at(&self, t_ns: u64) -> f64 {
62        let keys = self.keys();
63        let (Some(&(first_t, first_v)), Some(&(last_t, last_v))) = (keys.first(), keys.last())
64        else {
65            return 0.0;
66        };
67        if t_ns <= first_t {
68            return first_v;
69        }
70        if t_ns >= last_t {
71            return last_v;
72        }
73        // `t_ns` is strictly inside the range, so the neighbours both exist.
74        match self {
75            // The latest keyframe at or before `t_ns`: a keyframe's value takes
76            // effect exactly at its own time.
77            ControlSource::Step(_) => keys[keys.partition_point(|&(kt, _)| kt <= t_ns) - 1].1,
78            ControlSource::Linear(_) => {
79                let i = keys.partition_point(|&(kt, _)| kt < t_ns);
80                let (t1, v1) = keys[i];
81                let (t0, v0) = keys[i - 1];
82                // t0 < t_ns <= t1, so the span is non-zero.
83                let span = (t1 - t0) as f64;
84                let into = (t_ns - t0) as f64;
85                v0 + (v1 - v0) * (into / span)
86            }
87        }
88    }
89}
90
91fn sorted(keys: impl IntoIterator<Item = (u64, f64)>) -> Vec<(u64, f64)> {
92    let mut keys: Vec<(u64, f64)> = keys.into_iter().collect();
93    keys.sort_by_key(|&(t, _)| t);
94    keys
95}
96
97/// The animated properties of one graph node: property name -> curve. Attach it
98/// with [`Graph::set_node_control`](crate::Graph::set_node_control).
99#[derive(Debug, Clone, Default, PartialEq)]
100pub struct ControlProgram {
101    bindings: Vec<(String, ControlSource)>,
102}
103
104impl ControlProgram {
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Bind `property` to `source`. Re-binding a name replaces its curve, so a
110    /// program cannot hold two curves fighting over one property.
111    pub fn bind(mut self, property: &str, source: ControlSource) -> Self {
112        match self.bindings.iter_mut().find(|(n, _)| n == property) {
113            Some(slot) => slot.1 = source,
114            None => self.bindings.push((String::from(property), source)),
115        }
116        self
117    }
118
119    pub fn is_empty(&self) -> bool {
120        self.bindings.is_empty()
121    }
122
123    /// Check every binding against the target element's declared properties and
124    /// resolve each one's [`PropKind`], so the run's per-frame sampling is a
125    /// straight conversion with no name lookup. The first offending binding
126    /// fails.
127    pub fn resolve(self, specs: &[PropertySpec]) -> Result<ArmController, ControlFault> {
128        let mut bound = Vec::with_capacity(self.bindings.len());
129        for (property, source) in self.bindings {
130            let fault = |reason| ControlFault {
131                property: property.clone(),
132                reason,
133            };
134            if source.keys().is_empty() {
135                return Err(fault(ControlReason::NoKeyframes));
136            }
137            let spec = specs
138                .iter()
139                .find(|s| s.name == property)
140                .ok_or_else(|| fault(ControlReason::UnknownProperty))?;
141            if !animatable(spec.kind) {
142                return Err(fault(ControlReason::NotAnimatable(spec.kind)));
143            }
144            bound.push(Bound {
145                property,
146                kind: spec.kind,
147                source,
148            });
149        }
150        Ok(ArmController { bound })
151    }
152}
153
154/// The property kinds a curve can drive: the numeric ones, plus `Bool` as a
155/// threshold at 0.5. A fraction / string / flags property has no meaningful
156/// interpolation, so binding one is a startup error rather than a silent skip.
157fn animatable(kind: PropKind) -> bool {
158    matches!(
159        kind,
160        PropKind::Bool | PropKind::Int | PropKind::Uint | PropKind::Double
161    )
162}
163
164#[derive(Debug, Clone, PartialEq)]
165struct Bound {
166    property: String,
167    kind: PropKind,
168    source: ControlSource,
169}
170
171/// A [`ControlProgram`] resolved against its element, held by the arm that owns
172/// that element. The runner builds one per controlled node at startup and
173/// [`apply`](Self::apply)s it before each `DataFrame`.
174#[derive(Debug, Clone, PartialEq)]
175pub struct ArmController {
176    bound: Vec<Bound>,
177}
178
179impl ArmController {
180    /// Sample every binding at `pts_ns` and set it on `target`. An element that
181    /// rejects a sampled value fails the run loud: a curve that walks a property
182    /// out of its accepted range is a broken program, not something to swallow.
183    pub fn apply<T: ControlTarget + ?Sized>(
184        &self,
185        target: &mut T,
186        pts_ns: u64,
187    ) -> Result<(), ControlFault> {
188        for b in &self.bound {
189            let value = convert(b.kind, b.source.value_at(pts_ns));
190            target
191                .set_control(&b.property, value)
192                .map_err(|e| ControlFault {
193                    property: b.property.clone(),
194                    reason: ControlReason::Rejected(e),
195                })?;
196        }
197        Ok(())
198    }
199}
200
201/// Convert a sampled curve value to the property's kind: round to nearest and
202/// clamp into the kind's representable range (so a negative sample cannot wrap a
203/// `Uint`), `>= 0.5` for a `Bool`. `kind` is one [`animatable`] accepted.
204fn convert(kind: PropKind, sample: f64) -> PropValue {
205    match kind {
206        PropKind::Bool => PropValue::Bool(sample >= 0.5),
207        PropKind::Double => PropValue::Double(sample),
208        // `as` truncates toward zero, saturates at the integer bounds, and maps
209        // NaN to 0, so the half-step is what rounds (`f64::round` is a `std`
210        // method and the core is `no_std`, like `segment`'s `fabs`).
211        PropKind::Uint => PropValue::Uint(if sample <= 0.0 {
212            0
213        } else {
214            (sample + 0.5) as u64
215        }),
216        _ => PropValue::Int(if sample < 0.0 {
217            (sample - 0.5) as i64
218        } else {
219            (sample + 0.5) as i64
220        }),
221    }
222}
223
224/// Why a [`ControlProgram`] could not be resolved or applied, and to which
225/// property. The runner logs it and fails the run
226/// ([`G2gError::ControlBinding`](crate::G2gError::ControlBinding)).
227#[derive(Debug, Clone, PartialEq)]
228pub struct ControlFault {
229    pub property: String,
230    pub reason: ControlReason,
231}
232
233#[derive(Debug, Clone, PartialEq)]
234#[non_exhaustive]
235pub enum ControlReason {
236    /// The element declares no property of that name.
237    UnknownProperty,
238    /// The property exists but its kind carries no number to animate.
239    NotAnimatable(PropKind),
240    /// The binding has no keyframes, so there is nothing to sample.
241    NoKeyframes,
242    /// The element refused a sampled value.
243    Rejected(PropError),
244}
245
246impl core::fmt::Display for ControlFault {
247    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
248        match &self.reason {
249            ControlReason::UnknownProperty => {
250                write!(f, "no property named `{}` on this element", self.property)
251            }
252            ControlReason::NotAnimatable(kind) => write!(
253                f,
254                "property `{}` is a {} and cannot be animated",
255                self.property,
256                kind.label()
257            ),
258            ControlReason::NoKeyframes => {
259                write!(f, "property `{}` is bound to an empty curve", self.property)
260            }
261            ControlReason::Rejected(e) => write!(
262                f,
263                "property `{}` rejected a sampled value ({e})",
264                self.property
265            ),
266        }
267    }
268}
269
270/// The property surface a controller drives. Implemented for the erased element
271/// traits the runner's arms hold, so one sampling path serves a transform, a
272/// sink, and a fan-in element. Those traits are `std`-only (a `no_std` graph runs
273/// monomorphised elements), so the impls are too; an element type can implement
274/// this directly to be driven without them.
275pub trait ControlTarget {
276    fn set_control(&mut self, name: &str, value: PropValue) -> Result<(), PropError>;
277}
278
279#[cfg(feature = "std")]
280impl ControlTarget for dyn crate::element::DynAsyncElement + '_ {
281    fn set_control(&mut self, name: &str, value: PropValue) -> Result<(), PropError> {
282        self.set_property(name, value)
283    }
284}
285
286#[cfg(feature = "std")]
287impl ControlTarget for dyn crate::runtime::DynMultiInputElement + '_ {
288    fn set_control(&mut self, name: &str, value: PropValue) -> Result<(), PropError> {
289        self.set_property(name, value)
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    fn keys() -> [(u64, f64); 3] {
298        [(0, 0.0), (100, 10.0), (200, 20.0)]
299    }
300
301    #[test]
302    fn linear_interpolates_and_clamps_to_the_end_values() {
303        let s = ControlSource::linear(keys());
304        assert_eq!(s.value_at(0), 0.0);
305        assert_eq!(s.value_at(50), 5.0, "halfway between two keyframes");
306        assert_eq!(s.value_at(100), 10.0, "on a keyframe");
307        assert_eq!(s.value_at(150), 15.0);
308        assert_eq!(s.value_at(1_000), 20.0, "past the end holds the last value");
309    }
310
311    #[test]
312    fn step_holds_each_keyframe_until_the_next() {
313        let s = ControlSource::step(keys());
314        assert_eq!(s.value_at(0), 0.0);
315        assert_eq!(s.value_at(99), 0.0, "still the first value");
316        assert_eq!(s.value_at(100), 10.0);
317        assert_eq!(s.value_at(199), 10.0);
318        assert_eq!(s.value_at(500), 20.0);
319    }
320
321    #[test]
322    fn keyframes_are_sorted_at_construction() {
323        let s = ControlSource::linear([(200, 20.0), (0, 0.0), (100, 10.0)]);
324        assert_eq!(s.value_at(50), 5.0);
325    }
326
327    #[test]
328    fn conversion_rounds_clamps_and_thresholds() {
329        assert_eq!(convert(PropKind::Int, -2.4), PropValue::Int(-2));
330        assert_eq!(convert(PropKind::Int, 2.5), PropValue::Int(3));
331        assert_eq!(
332            convert(PropKind::Uint, -7.0),
333            PropValue::Uint(0),
334            "a negative sample clamps instead of wrapping"
335        );
336        assert_eq!(convert(PropKind::Bool, 0.49), PropValue::Bool(false));
337        assert_eq!(convert(PropKind::Bool, 0.5), PropValue::Bool(true));
338        assert_eq!(convert(PropKind::Double, 1.25), PropValue::Double(1.25));
339    }
340
341    #[test]
342    fn rebinding_a_property_replaces_its_curve() {
343        let p = ControlProgram::new()
344            .bind("x", ControlSource::step([(0, 1.0)]))
345            .bind("x", ControlSource::step([(0, 2.0)]));
346        assert_eq!(p.bindings.len(), 1);
347        assert_eq!(p.bindings[0].1.value_at(0), 2.0);
348    }
349}