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}