conformal_component/parameters/utils/
per_sample.rs

1use super::super::{
2    EnumBufferState, NumericBufferState, PiecewiseLinearCurve, PiecewiseLinearCurvePoint,
3    SwitchBufferState, TimedEnumValues, TimedSwitchValues, TimedValue,
4};
5
6fn piecewise_linear_curve_per_sample<
7    I: IntoIterator<Item = PiecewiseLinearCurvePoint, IntoIter: Clone>,
8>(
9    curve: PiecewiseLinearCurve<I>,
10) -> impl Iterator<Item = f32> + Clone {
11    let buffer_size = curve.buffer_size();
12    let mut i = curve.into_iter();
13    let mut next = i.next();
14    let mut last = None;
15    let mut last_sample_offset = 0;
16    (0..buffer_size).map(move |idx| {
17        if let Some(PiecewiseLinearCurvePoint {
18            sample_offset,
19            value,
20        }) = next
21        {
22            if sample_offset == idx {
23                last = Some(value);
24                last_sample_offset = sample_offset;
25                next = i.next();
26                value
27            } else {
28                // unwrap is safe here because we know there is a point at zero.
29                let delta = value - last.unwrap();
30
31                // Note that we will fix any rounding errors when we hit the next point,
32                // so we allow a lossy cast in the next block.
33                #[allow(clippy::cast_precision_loss)]
34                {
35                    let delta_per_sample = delta / ((sample_offset - last_sample_offset) as f32);
36
37                    last.unwrap() + delta_per_sample * ((idx - last_sample_offset) as f32)
38                }
39            }
40        } else {
41            // Unwrap is safe here because we know that there is at least one point in curve.
42            last.unwrap()
43        }
44    })
45}
46
47#[doc(hidden)]
48pub fn decompose_numeric<I: IntoIterator<Item = PiecewiseLinearCurvePoint, IntoIter: Clone>>(
49    state: NumericBufferState<I>,
50) -> (f32, Option<impl Iterator<Item = f32> + Clone>) {
51    match state {
52        NumericBufferState::Constant(v) => (v, None),
53        NumericBufferState::PiecewiseLinear(c) => (0.0, Some(piecewise_linear_curve_per_sample(c))),
54    }
55}
56
57/// Converts a [`NumericBufferState`] into a per-sample iterator.
58///
59/// This provides the value of the parameter at each sample in the buffer.
60/// Note: for constant values, this returns an infinite iterator.
61pub fn numeric_per_sample<I: IntoIterator<Item = PiecewiseLinearCurvePoint, IntoIter: Clone>>(
62    state: NumericBufferState<I>,
63) -> impl Iterator<Item = f32> + Clone {
64    match state {
65        NumericBufferState::Constant(v) => itertools::Either::Left(core::iter::repeat(v)),
66        NumericBufferState::PiecewiseLinear(c) => {
67            itertools::Either::Right(piecewise_linear_curve_per_sample(c))
68        }
69    }
70}
71
72#[allow(clippy::missing_panics_doc)] // We only panic when invariants are broken.
73fn timed_enum_per_sample<I: IntoIterator<Item = TimedValue<u32>, IntoIter: Clone>>(
74    values: TimedEnumValues<I>,
75) -> impl Iterator<Item = u32> + Clone {
76    let buffer_size = values.buffer_size();
77    let mut i = values.into_iter();
78    let mut next = i.next();
79    let mut last = None;
80    (0..buffer_size).map(move |idx| {
81        if let Some(TimedValue {
82            sample_offset,
83            value,
84        }) = next
85        {
86            if sample_offset == idx {
87                last = Some(value);
88                next = i.next();
89                value
90            } else {
91                // unwrap is safe here because we know there is a point at zero.
92                last.unwrap()
93            }
94        } else {
95            // Unwrap is safe here because we know that there is at least one point in curve.
96            last.unwrap()
97        }
98    })
99}
100
101#[doc(hidden)]
102pub fn decompose_enum<I: IntoIterator<Item = TimedValue<u32>, IntoIter: Clone>>(
103    state: EnumBufferState<I>,
104) -> (u32, Option<impl Iterator<Item = u32> + Clone>) {
105    match state {
106        EnumBufferState::Constant(v) => (v, None),
107        EnumBufferState::Varying(c) => (0, Some(timed_enum_per_sample(c))),
108    }
109}
110
111/// Converts an [`EnumBufferState`] into a per-sample iterator.
112///
113/// This provides the value of the parameter at each sample in the buffer.
114/// Note: for constant values, this returns an infinite iterator.
115pub fn enum_per_sample<I: IntoIterator<Item = TimedValue<u32>, IntoIter: Clone>>(
116    state: EnumBufferState<I>,
117) -> impl Iterator<Item = u32> + Clone {
118    match state {
119        EnumBufferState::Constant(v) => itertools::Either::Left(core::iter::repeat(v)),
120        EnumBufferState::Varying(c) => itertools::Either::Right(timed_enum_per_sample(c)),
121    }
122}
123
124#[allow(clippy::missing_panics_doc)] // We only panic when invariants are broken.
125fn timed_switch_per_sample<I: IntoIterator<Item = TimedValue<bool>, IntoIter: Clone>>(
126    values: TimedSwitchValues<I>,
127) -> impl Iterator<Item = bool> + Clone {
128    let buffer_size = values.buffer_size();
129    let mut i = values.into_iter();
130    let mut next = i.next();
131    let mut last = None;
132    (0..buffer_size).map(move |idx| {
133        if let Some(TimedValue {
134            sample_offset,
135            value,
136        }) = next
137        {
138            if sample_offset == idx {
139                last = Some(value);
140                next = i.next();
141                value
142            } else {
143                // unwrap is safe here because we know there is a point at zero.
144                last.unwrap()
145            }
146        } else {
147            // Unwrap is safe here because we know that there is at least one point in curve.
148            last.unwrap()
149        }
150    })
151}
152
153#[doc(hidden)]
154pub fn decompose_switch<I: IntoIterator<Item = TimedValue<bool>, IntoIter: Clone>>(
155    state: SwitchBufferState<I>,
156) -> (bool, Option<impl Iterator<Item = bool> + Clone>) {
157    match state {
158        SwitchBufferState::Constant(v) => (v, None),
159        SwitchBufferState::Varying(c) => (false, Some(timed_switch_per_sample(c))),
160    }
161}
162
163/// Converts a [`SwitchBufferState`] into a per-sample iterator.
164///
165/// This provides the value of the parameter at each sample in the buffer.
166/// Note: for constant values, this returns an infinite iterator.
167pub fn switch_per_sample<I: IntoIterator<Item = TimedValue<bool>, IntoIter: Clone>>(
168    state: SwitchBufferState<I>,
169) -> impl Iterator<Item = bool> + Clone {
170    match state {
171        SwitchBufferState::Constant(v) => itertools::Either::Left(core::iter::repeat(v)),
172        SwitchBufferState::Varying(c) => itertools::Either::Right(timed_switch_per_sample(c)),
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use crate::audio::all_approx_eq;
179
180    use super::super::super::{
181        PiecewiseLinearCurve, PiecewiseLinearCurvePoint, TimedEnumValues, TimedSwitchValues,
182        TimedValue,
183    };
184    use super::{
185        piecewise_linear_curve_per_sample, timed_enum_per_sample, timed_switch_per_sample,
186    };
187
188    const TEST_EPSILON: f32 = 1e-7;
189
190    #[test]
191    fn piecewise_linear_curve_per_sample_basics() {
192        let vals = piecewise_linear_curve_per_sample(
193            PiecewiseLinearCurve::new(
194                (&[
195                    PiecewiseLinearCurvePoint {
196                        sample_offset: 0,
197                        value: 0.0,
198                    },
199                    PiecewiseLinearCurvePoint {
200                        sample_offset: 5,
201                        value: 5.0,
202                    },
203                    PiecewiseLinearCurvePoint {
204                        sample_offset: 7,
205                        value: 5.0,
206                    },
207                    PiecewiseLinearCurvePoint {
208                        sample_offset: 8,
209                        value: 10.0,
210                    },
211                ])
212                    .iter()
213                    .cloned(),
214                10,
215                0.0..=10.0,
216            )
217            .unwrap(),
218        )
219        .collect::<Vec<_>>();
220        assert!(all_approx_eq(
221            vals.iter().copied(),
222            ([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 10.0, 10.0])
223                .iter()
224                .copied(),
225            TEST_EPSILON
226        ));
227    }
228
229    #[test]
230    fn timed_enum_per_sample_basics() {
231        let vals = timed_enum_per_sample(
232            TimedEnumValues::new(
233                (&[
234                    TimedValue {
235                        sample_offset: 0,
236                        value: 0,
237                    },
238                    TimedValue {
239                        sample_offset: 7,
240                        value: 2,
241                    },
242                    TimedValue {
243                        sample_offset: 8,
244                        value: 3,
245                    },
246                ])
247                    .iter()
248                    .cloned(),
249                10,
250                0..4,
251            )
252            .unwrap(),
253        )
254        .collect::<Vec<_>>();
255        assert!(
256            vals.iter()
257                .copied()
258                .zip(([0, 0, 0, 0, 0, 0, 0, 2, 3, 3]).iter().copied())
259                .all(|(a, b)| a == b)
260        );
261    }
262
263    #[test]
264    fn timed_switch_per_sample_basics() {
265        let vals = timed_switch_per_sample(
266            TimedSwitchValues::new(
267                (&[
268                    TimedValue {
269                        sample_offset: 0,
270                        value: false,
271                    },
272                    TimedValue {
273                        sample_offset: 7,
274                        value: true,
275                    },
276                    TimedValue {
277                        sample_offset: 8,
278                        value: false,
279                    },
280                ])
281                    .iter()
282                    .cloned(),
283                10,
284            )
285            .unwrap(),
286        )
287        .collect::<Vec<_>>();
288        assert!(
289            vals.iter()
290                .copied()
291                .zip(
292                    ([
293                        false, false, false, false, false, false, false, true, false, false
294                    ])
295                    .iter()
296                    .copied()
297                )
298                .all(|(a, b)| a == b)
299        );
300    }
301}