Skip to main content

servo_media_audio/
param.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cmp::Ordering;
6
7use malloc_size_of_derive::MallocSizeOf;
8
9use crate::audio_node::BlockInfo;
10use crate::block::{Block, FRAMES_PER_BLOCK_USIZE, Tick};
11
12#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, MallocSizeOf)]
13pub enum ParamType {
14    Frequency,
15    Detune,
16    DelayTime,
17    Gain,
18    Q,
19    Pan,
20    PlaybackRate,
21    Position(ParamDir),
22    Forward(ParamDir),
23    Up(ParamDir),
24    Orientation(ParamDir),
25    Offset,
26}
27
28#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd, MallocSizeOf)]
29pub enum ParamDir {
30    X,
31    Y,
32    Z,
33}
34
35/// An AudioParam.
36///
37/// <https://webaudio.github.io/web-audio-api/#AudioParam>
38#[derive(Debug)]
39pub struct Param {
40    val: f32,
41    /// Time of the value update
42    time: Tick,
43    default_value: f32,
44    val_range: (f32, f32),
45    kind: ParamRate,
46    /// AutomationEvent and Cancel Hold Tick, if applicable
47    events: Vec<AutomationEvent>,
48    current_event: usize,
49    event_start_time: Tick,
50    event_start_value: f32,
51    /// Cache of inputs from connect()ed nodes
52    blocks: Vec<Block>,
53    /// The value of all connect()ed inputs mixed together, for this frame
54    block_mix_val: f32,
55    /// If true, `blocks` has been summed together into a single block
56    summed: bool,
57    dirty: bool,
58}
59
60#[derive(Copy, Clone, Eq, PartialEq, Debug, MallocSizeOf)]
61pub enum ParamRate {
62    /// Value is held for entire block
63    KRate,
64    /// Value is updated each frame
65    ARate,
66}
67
68impl Param {
69    pub fn new(val: f32) -> Self {
70        Param {
71            val,
72            time: Tick(0),
73            default_value: val,
74            val_range: (f32::MIN, f32::MAX),
75            kind: ParamRate::ARate,
76            events: vec![],
77            current_event: 0,
78            event_start_time: Tick(0),
79            event_start_value: val,
80            blocks: Vec::new(),
81            block_mix_val: 0.,
82            summed: false,
83            dirty: false,
84        }
85    }
86
87    pub fn new_krate(val: f32) -> Self {
88        Param {
89            val,
90            time: Tick(0),
91            default_value: val,
92            val_range: (f32::MIN, f32::MAX),
93            kind: ParamRate::KRate,
94            events: vec![],
95            current_event: 0,
96            event_start_time: Tick(0),
97            event_start_value: val,
98            blocks: Vec::new(),
99            block_mix_val: 0.,
100            summed: false,
101            dirty: false,
102        }
103    }
104
105    fn get_previous_event(&self, index: usize) -> Option<&AutomationEvent> {
106        if index > 0 {
107            self.events.get(index - 1)
108        } else {
109            None
110        }
111    }
112
113    fn get_previous_event_mut(&mut self, index: usize) -> Option<&mut AutomationEvent> {
114        if index > 0 {
115            self.events.get_mut(index - 1)
116        } else {
117            None
118        }
119    }
120
121    /// Update the value of this param to the next
122    ///
123    /// Invariant: This should be called with monotonically increasing
124    /// ticks, and Tick(0) should never be skipped.
125    ///
126    /// Returns true if anything changed
127    pub fn update(&mut self, block: &BlockInfo, tick: Tick) -> bool {
128        let mut changed = self.dirty;
129        self.dirty = false;
130        if tick.0 == 0 {
131            self.summed = true;
132            if let Some(first) = self.blocks.pop() {
133                // first sum them together
134                // https://webaudio.github.io/web-audio-api/#dom-audionode-connect-destinationparam-output
135                let block = self
136                    .blocks
137                    .drain(..)
138                    .fold(first, |acc, block| acc.sum(block));
139                self.blocks.push(block);
140            }
141        } else if self.kind == ParamRate::KRate {
142            return changed;
143        }
144
145        // Even if the timeline does nothing, it's still possible
146        // that there were connected inputs, so we should not
147        // directly return `false` after this point, instead returning
148        // `changed`
149        changed |= if let Some(block) = self.blocks.first() {
150            // store to be summed with `val` later
151            self.block_mix_val = block.data_chan_frame(tick.0 as usize, 0);
152            true
153        } else {
154            false
155        };
156
157        if self.events.len() <= self.current_event {
158            return changed;
159        }
160
161        let current_tick = block.absolute_tick(tick);
162        let mut current_event = &self.events[self.current_event];
163
164        // Update then move to next event if necessary.
165        // This will also handle cases where we have overlap,
166        // ie. two ramps with the same end time.
167        loop {
168            let maybe_next_event = self.events.get(self.current_event + 1);
169            // Run the event automation if it is active.
170            if current_event.is_active(
171                current_tick,
172                self.get_previous_event(self.current_event),
173                maybe_next_event,
174            ) {
175                if let Some(start_time) = current_event.start_time() {
176                    // If the current event just started, update the param event start time and val
177                    if start_time == current_tick {
178                        self.event_start_time = self.time;
179                        self.event_start_value = self.val;
180                        // > If the preceding event is a SetTarget event,
181                        // > T0 and V0 are chosen from the current time and value of SetTarget automation.
182                        // > That is, if the SetTarget event has not started, T0 is the start time of the event,
183                        // > and V0 is the value just before the SetTarget event starts.
184                        // > In this case, the LinearRampToValue event effectively replaces the SetTarget event.
185                        if current_event.is_set_target() &&
186                            maybe_next_event
187                                .map(|next_event| next_event.is_ramp())
188                                .unwrap_or_default()
189                        {
190                            continue;
191                        }
192                    }
193                }
194                // Update the value
195                if current_event.run(
196                    &mut self.val,
197                    &mut self.time,
198                    current_tick,
199                    self.event_start_time,
200                    self.event_start_value,
201                ) {
202                    // If at least one event updated the param value, then changed is true.
203                    changed = true;
204                }
205            }
206            // Continue to the next event if the next event is active.
207            if let Some(next_event) = maybe_next_event &&
208                next_event.is_active(
209                    current_tick,
210                    Some(current_event),
211                    self.events.get(self.current_event + 2),
212                )
213            {
214                self.current_event += 1;
215                // If the next event is a ramp, set the param event start value and time.
216                // This is because ramp does not have a start time.
217                if next_event.is_ramp() {
218                    self.event_start_time = self.time;
219                    self.event_start_value = self.val;
220                }
221                current_event = next_event;
222                continue;
223            }
224            break;
225        }
226        changed
227    }
228
229    pub fn value(&self) -> f32 {
230        // the data from connect()ed audionodes is first mixed
231        // together in update(), and then mixed with the actual param value
232        // https://webaudio.github.io/web-audio-api/#dom-audionode-connect-destinationparam-output
233        // Clamp values when they are to be applied to the output, not during automation.
234        // > If the sum is NaN, replace the sum with the defaultValue.
235        // Specs do not mention replacing positive / negative infinity with defaultValue.
236        // However, nan-param.html WPT test suggests this behavior is expected.
237        // https://webaudio.github.io/web-audio-api/#computedvalue
238        let mut computed_value = self.val + self.block_mix_val;
239        if !computed_value.is_finite() {
240            computed_value = self.default_value;
241        }
242        computed_value.clamp(self.val_range.0, self.val_range.1)
243    }
244
245    pub fn set_rate(&mut self, rate: ParamRate) {
246        self.kind = rate;
247    }
248
249    pub(crate) fn update_range(&mut self, range: (f32, f32)) {
250        self.val_range = range;
251    }
252
253    pub(crate) fn insert_event(&mut self, event: AutomationEvent) {
254        if let AutomationEvent::SetValue(val) = event {
255            self.val = val;
256            self.event_start_value = val;
257            self.dirty = true;
258            return;
259        }
260
261        let time = event.time();
262
263        // > If one of these events is added at a time where there is already one or more events,
264        // > then it will be placed in the list after them, but before events whose times are after the event.
265        // https://webaudio.github.io/web-audio-api/#dfn-automation-method
266        let result = self.events.binary_search_by(|e| {
267            if e.time() <= time {
268                Ordering::Less
269            } else {
270                Ordering::Greater
271            }
272        });
273        let idx = match result {
274            // Through custom comparator, binary search will never find a match.
275            // It will always find the largest index with time greater than or equal to the event.
276            Ok(_) => unreachable!(),
277            Err(idx) => idx,
278        };
279
280        if let Some(is_hold) = event.cancel_event() {
281            let mut events_to_keep = idx;
282            // https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelandholdattime
283            if is_hold {
284                // > the automation value that would have happened at cancelTime
285                // > is then propagated for all future time until other automation events are introduced.
286                //
287                // If we have a hold, set the cancel hold tick for the event.
288                // When updating the param, once we reach the hold tick, we will hold the current value.
289                if let Some(overlap_event) = self.events.get_mut(idx) &&
290                    overlap_event.is_ramp()
291                {
292                    overlap_event.set_cancel_tick(event.time());
293                    // Keep the RampToValueAtTime event.
294                    events_to_keep = idx + 1;
295                } else {
296                    if let Some(overlap_event) = self.get_previous_event_mut(idx) &&
297                        (overlap_event.is_set_target() ||
298                            // SetValueCurve always has done time
299                            (overlap_event.is_set_value_curve() &&
300                                overlap_event
301                                    .done_time()
302                                    .unwrap() >=
303                                    event.time()))
304                    {
305                        overlap_event.set_cancel_tick(event.time());
306                    }
307                }
308                self.events.truncate(events_to_keep);
309            } else {
310                // If the event preceding the cancel is not setValueAtTime,
311                // then it is an active event.
312                // > Any active automations whose automation event time is less
313                // > than cancelTime are also cancelled.
314                // https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelscheduledvalues
315                let mut overlap_start_time = None;
316                if let Some(overlap_event) = self.get_previous_event(idx) &&
317                    (overlap_event.is_set_target() ||
318                        (overlap_event.is_set_value_curve() &&
319                            // SetValueCurve always has done time
320                            overlap_event
321                                .done_time()
322                                .unwrap() >=
323                                event.time()))
324                {
325                    events_to_keep = idx - 1;
326                    overlap_start_time = Some(overlap_event.time());
327                }
328                self.events.truncate(events_to_keep);
329                // If the current value was set after the overlap start time,
330                // then we need to reset the value to before the active event started.
331                // > cancellations may cause discontinuities because
332                // > the original value (from before such automation) is restored immediately.
333                if let Some(overlap_start_time) = overlap_start_time &&
334                    self.time > overlap_start_time
335                {
336                    self.val = self.event_start_value;
337                    self.time = self.event_start_time;
338                }
339            }
340            // don't actually insert the event
341            return;
342        }
343        self.events.insert(idx, event);
344        // XXXManishearth handle inserting events with a time before that
345        // of the current one
346    }
347
348    pub(crate) fn add_block(&mut self, block: Block) {
349        debug_assert!(block.chan_count() == 1);
350        // summed only becomes true during a node's process() call,
351        // but add_block is called during graph traversal before processing,
352        // so if summed is true that means we've moved on to the next block
353        // and should clear our inputs
354        if self.summed {
355            self.blocks.clear();
356        }
357        self.blocks.push(block)
358    }
359
360    /// Flush an entire block of values into a buffer
361    ///
362    /// Only for use with AudioListener.
363    ///
364    /// Invariant: `block` must be a FRAMES_PER_BLOCK length array filled with silence
365    pub(crate) fn flush_to_block(&mut self, info: &BlockInfo, block: &mut [f32]) {
366        // common case
367        if self.current_event >= self.events.len() && self.blocks.is_empty() {
368            if self.val != 0. {
369                for block_tick in &mut block[0..FRAMES_PER_BLOCK_USIZE] {
370                    // ideally this can use some kind of vectorized memset()
371                    *block_tick = self.val;
372                }
373            }
374        // if the value is zero, our buffer is already zeroed
375        } else {
376            for block_tick in &mut block[0..FRAMES_PER_BLOCK_USIZE] {
377                self.update(info, Tick(*block_tick as u64));
378                *block_tick = self.val;
379            }
380        }
381    }
382}
383
384#[derive(Clone, Copy, Eq, PartialEq, Debug, MallocSizeOf)]
385pub enum RampKind {
386    Linear,
387    Exponential,
388}
389
390#[derive(Clone, PartialEq, Debug)]
391/// <https://webaudio.github.io/web-audio-api/#dfn-automation-event>
392pub(crate) enum AutomationEvent {
393    SetValue(f32),
394    SetValueAtTime(f32, Tick),
395    RampToValueAtTime(
396        RampKind,
397        f32,
398        Tick,
399        /* Cancel and Hold Tick */ Option<Tick>,
400    ),
401    SetTargetAtTime(
402        f32,
403        Tick,
404        /* time constant, units of Tick */
405        f64,
406        // Cancel and Hold Tick
407        Option<Tick>,
408    ),
409    SetValueCurveAtTime(
410        Vec<f32>,
411        /* start time */ Tick,
412        /* duration */ Tick,
413        // Cancel and Hold Tick
414        Option<Tick>,
415    ),
416    CancelAndHoldAtTime(Tick),
417    CancelScheduledValues(Tick),
418}
419
420#[derive(Clone, PartialEq, Debug, MallocSizeOf)]
421/// An AutomationEvent that uses times in s instead of Ticks
422pub enum UserAutomationEvent {
423    SetValue(f32),
424    SetValueAtTime(f32, /* time */ f64),
425    RampToValueAtTime(RampKind, f32, /* time */ f64),
426    SetTargetAtTime(f32, f64, /* time constant, units of s */ f64),
427    SetValueCurveAtTime(Vec<f32>, /* start time */ f64, /* duration */ f64),
428    CancelAndHoldAtTime(f64),
429    CancelScheduledValues(f64),
430}
431
432impl UserAutomationEvent {
433    pub(crate) fn convert_to_event(self, rate: f32) -> AutomationEvent {
434        match self {
435            UserAutomationEvent::SetValue(val) => AutomationEvent::SetValue(val),
436            UserAutomationEvent::SetValueAtTime(val, time) => {
437                AutomationEvent::SetValueAtTime(val, Tick::from_time(time, rate))
438            },
439            UserAutomationEvent::RampToValueAtTime(kind, val, time) => {
440                AutomationEvent::RampToValueAtTime(kind, val, Tick::from_time(time, rate), None)
441            },
442            UserAutomationEvent::SetValueCurveAtTime(values, start, duration) => {
443                AutomationEvent::SetValueCurveAtTime(
444                    values,
445                    Tick::from_time(start, rate),
446                    Tick::from_time(duration, rate),
447                    None,
448                )
449            },
450            UserAutomationEvent::SetTargetAtTime(val, start, tau) => {
451                AutomationEvent::SetTargetAtTime(
452                    val,
453                    Tick::from_time(start, rate),
454                    tau * rate as f64,
455                    None,
456                )
457            },
458            UserAutomationEvent::CancelScheduledValues(t) => {
459                AutomationEvent::CancelScheduledValues(Tick::from_time(t, rate))
460            },
461            UserAutomationEvent::CancelAndHoldAtTime(t) => {
462                AutomationEvent::CancelAndHoldAtTime(Tick::from_time(t, rate))
463            },
464        }
465    }
466}
467
468impl AutomationEvent {
469    /// The time of the event used for ordering
470    pub fn time(&self) -> Tick {
471        match *self {
472            AutomationEvent::SetValueAtTime(_, tick) => tick,
473            AutomationEvent::SetValueCurveAtTime(_, start, _, _) => start,
474            AutomationEvent::RampToValueAtTime(_, _, tick, cancel_tick) => {
475                cancel_tick.unwrap_or(tick)
476            },
477            AutomationEvent::SetTargetAtTime(_, start, _, _) => start,
478            AutomationEvent::CancelAndHoldAtTime(t) => t,
479            AutomationEvent::CancelScheduledValues(tick) => tick,
480            AutomationEvent::SetValue(..) => {
481                unreachable!("SetValue should never appear in the timeline")
482            },
483        }
484    }
485
486    pub fn done_time(&self) -> Option<Tick> {
487        match *self {
488            AutomationEvent::SetValueAtTime(_, tick) => Some(tick),
489            AutomationEvent::RampToValueAtTime(_, _, tick, cancel_tick) => {
490                Some(cancel_tick.unwrap_or(tick))
491            },
492            AutomationEvent::SetValueCurveAtTime(_, start, duration, cancel_tick) => {
493                Some(cancel_tick.unwrap_or(start + duration))
494            },
495            AutomationEvent::SetTargetAtTime(_, _, _, cancel_tick) => cancel_tick,
496            AutomationEvent::CancelAndHoldAtTime(t) => Some(t),
497            AutomationEvent::CancelScheduledValues(..) | AutomationEvent::SetValue(..) => {
498                unreachable!("CancelScheduledValues/SetValue should never appear in the timeline")
499            },
500        }
501    }
502
503    pub fn start_time(&self) -> Option<Tick> {
504        match *self {
505            AutomationEvent::SetValueAtTime(_, tick) => Some(tick),
506            AutomationEvent::RampToValueAtTime(..) => None,
507            AutomationEvent::SetValueCurveAtTime(_, start, _, _) => Some(start),
508            AutomationEvent::SetTargetAtTime(_, start, _, _) => Some(start),
509            AutomationEvent::CancelAndHoldAtTime(t) => Some(t),
510            AutomationEvent::CancelScheduledValues(..) | AutomationEvent::SetValue(..) => {
511                unreachable!("CancelScheduledValues/SetValue should never appear in the timeline")
512            },
513        }
514    }
515
516    /// Returns Some if it's a cancel event
517    /// the boolean is if it's CancelAndHold
518    pub fn cancel_event(&self) -> Option<bool> {
519        match *self {
520            AutomationEvent::CancelAndHoldAtTime(..) => Some(true),
521            AutomationEvent::CancelScheduledValues(..) => Some(false),
522            _ => None,
523        }
524    }
525
526    fn is_ramp(&self) -> bool {
527        matches!(*self, AutomationEvent::RampToValueAtTime(..))
528    }
529
530    fn is_set_target(&self) -> bool {
531        matches!(*self, AutomationEvent::SetTargetAtTime(..))
532    }
533
534    fn is_set_value_curve(&self) -> bool {
535        matches!(*self, AutomationEvent::SetValueCurveAtTime(..))
536    }
537
538    fn is_active(
539        &self,
540        current_tick: Tick,
541        previous_event: Option<&AutomationEvent>,
542        next_event: Option<&AutomationEvent>,
543    ) -> bool {
544        match self {
545            AutomationEvent::SetValueAtTime(_, time) => *time == current_tick,
546            // https://webaudio.github.io/web-audio-api/#dom-audioparam-linearramptovalueattime
547            AutomationEvent::RampToValueAtTime(..) => {
548                // Ramp always has a done time.
549                let done_time = self.done_time().unwrap();
550                let previous_event_check = previous_event.is_none_or(|event| {
551                    // > If the preceding event is a SetTarget event, T0 and V0
552                    // > are chosen from the current time and value of SetTarget automation.
553                    // > If the SetTarget event has already started, T0 is the current context time,
554                    // > and V0 is the current SetTarget automation value at time T0.
555                    //
556                    // If previous event is an active SetTarget, then this ramp will be active,
557                    // because T0 is set to current time.
558                    if event.is_set_target() {
559                        event.is_active(current_tick, None, None)
560                    } else {
561                        // > The value during the time interval T0≤t<T1
562                        // > (where T0 is the time of the previous event
563                        // > and T1 is the endTime parameter passed into this method) will be calculated as:
564                        // > v(t)=V0+(V1−V0)*(t−T0)/(T1−T0)
565                        // > where V0 is the value at the time T0 and V1 is the value parameter passed into this method.
566                        //
567                        // This means the event is not active until T0 <= t.
568                        event.time() <= current_tick
569                    }
570                });
571                // > If there are no more events after this LinearRampToValue event then for t≥T1, v(t)=V1.
572                //
573                // This means if there are no active events after this, the event is active at T1.
574                // The specs are not explicit about what happens if the next event starts at T1.
575                // Based on behavior from Firefox and Chrome, v(t)=V1 for t=T1 even if next event
576                // starts at T1.
577                // Therefore we consider the event active at T1.
578                done_time >= current_tick && previous_event_check
579            },
580            // > During the time interval: T0≤t, where T0 is the startTime parameter.
581            // https://webaudio.github.io/web-audio-api/#dom-audioparam-settargetattime
582            AutomationEvent::SetTargetAtTime(_, start_time, _, _) => {
583                // SetTarget has a start time. Once current tick reaches the start time,
584                // the event becomes active.
585                let next_event_check = next_event.is_none_or(|event| {
586                    // If next event is Ramp, it will become active once the SetTarget is active.
587                    // We need to handle the edge case where the SetTarget event is already started.
588                    // In this case, as mentioned above, the value as of current context time will
589                    // be used by the ramp as V0.
590                    // Therefore this should return true as we would always want to calculate the value,
591                    // assuming the other checks pass.
592                    event.is_ramp() ||
593                        // > For all other events, the SetTarget event ends at the time of the next event.
594                        // So event is active until next event starts
595                        event.time() > current_tick
596                });
597                // https://webaudio.github.io/web-audio-api/#dom-audioparam-cancelandholdattime
598                // Calculate based on automation, and then at t_c the value calculated will be held.
599                let cancel_and_hold_check = self
600                    .done_time()
601                    .is_none_or(|done_time| done_time >= current_tick);
602
603                *start_time <= current_tick && next_event_check && cancel_and_hold_check
604            },
605            // https://webaudio.github.io/web-audio-api/#dom-audioparam-setvaluecurveattime
606            AutomationEvent::SetValueCurveAtTime(_, start_time, _, _) => {
607                // > An implicit call to setValueAtTime() is made at time T0+TD with value V[N−1]
608                // > so that following automations will start from the end of the setValueCurveAtTime() event.
609                //
610                // Therefore when t = T0+TD, it is active because we need to calculate V(t).
611                *start_time <=
612                    current_tick &&
613                    // SetValueCurve always has an end time.
614                    self.done_time()
615                        .unwrap() >=
616                        current_tick
617            },
618            _ => false,
619        }
620    }
621
622    fn set_cancel_tick(&mut self, cancel_tick: Tick) {
623        match self {
624            AutomationEvent::RampToValueAtTime(_, _, _, maybe_tick) |
625            AutomationEvent::SetTargetAtTime(_, _, _, maybe_tick) |
626            AutomationEvent::SetValueCurveAtTime(_, _, _, maybe_tick) => {
627                maybe_tick.replace(cancel_tick);
628            },
629            _ => {},
630        }
631    }
632
633    /// Update a parameter based on this event
634    ///
635    /// Returns true if something changed
636    pub fn run(
637        &self,
638        value: &mut f32,
639        value_time: &mut Tick,
640        current_tick: Tick,
641        event_start_time: Tick,
642        event_start_value: f32,
643    ) -> bool {
644        if matches!(self.start_time(), Some(start_time) if start_time > current_tick) {
645            // The previous event finished and we advanced to this
646            // event, but it's not started yet. Return early
647            return false;
648        }
649
650        match *self {
651            AutomationEvent::SetValueAtTime(val, time) => {
652                if current_tick == time {
653                    *value = val;
654                    *value_time = time;
655                    true
656                } else {
657                    false
658                }
659            },
660            AutomationEvent::RampToValueAtTime(kind, val, time, _) => {
661                if time == event_start_time {
662                    *value = val;
663                } else {
664                    let progress = (current_tick - event_start_time).0 as f32 /
665                        (time - event_start_time).0 as f32;
666                    match kind {
667                        RampKind::Linear => {
668                            *value = event_start_value + (val - event_start_value) * progress;
669                        },
670                        RampKind::Exponential => {
671                            let ratio = val / event_start_value;
672                            if event_start_value == 0. || ratio < 0. {
673                                if time == current_tick {
674                                    *value = val;
675                                } else {
676                                    *value = event_start_value;
677                                }
678                            } else {
679                                *value = event_start_value * (ratio).powf(progress);
680                            }
681                        },
682                    }
683                }
684                *value_time = current_tick;
685                true
686            },
687            AutomationEvent::SetTargetAtTime(val, start, tau, _) => {
688                let exp = -((current_tick - start) / tau);
689                *value = val + (event_start_value - val) * exp.exp() as f32;
690                *value_time = current_tick;
691                true
692            },
693            AutomationEvent::SetValueCurveAtTime(ref values, start, duration, _) => {
694                let progress = ((current_tick.0 as f32) - (start.0 as f32)) / (duration.0 as f32);
695                debug_assert!(progress >= 0.);
696                let n = values.len() as f32;
697                let k_float = (n - 1.) * progress;
698                let k = k_float.floor();
699                if (k + 1.) < n {
700                    let progress = k_float - k;
701                    *value =
702                        values[k as usize] * (1. - progress) + values[(k + 1.) as usize] * progress;
703                } else {
704                    *value = values[(n - 1.) as usize];
705                }
706                *value_time = current_tick;
707                true
708            },
709            AutomationEvent::CancelAndHoldAtTime(..) => false,
710            AutomationEvent::CancelScheduledValues(..) | AutomationEvent::SetValue(..) => {
711                unreachable!("CancelScheduledValues/SetValue should never appear in the timeline")
712            },
713        }
714    }
715}