autd3_core/sampling_config/
mod.rs

1mod error;
2
3use std::{fmt::Debug, num::NonZeroU16};
4
5use crate::{
6    defined::{Freq, Hz, ultrasound_freq},
7    utils::float::is_integer,
8};
9
10pub use error::SamplingConfigError;
11
12/// Nearest type.
13#[derive(Copy, Clone, Debug, PartialEq)]
14pub struct Nearest<T: Copy + Clone + Debug + PartialEq>(pub T);
15
16/// The configuration for sampling.
17#[derive(Clone, Copy)]
18pub enum SamplingConfig {
19    #[doc(hidden)]
20    Division(NonZeroU16),
21    #[doc(hidden)]
22    Freq(Freq<f32>),
23    #[cfg(not(feature = "dynamic_freq"))]
24    #[doc(hidden)]
25    Period(std::time::Duration),
26    #[doc(hidden)]
27    FreqNearest(Nearest<Freq<f32>>),
28    #[cfg(not(feature = "dynamic_freq"))]
29    #[doc(hidden)]
30    PeriodNearest(Nearest<std::time::Duration>),
31}
32
33impl PartialEq for SamplingConfig {
34    fn eq(&self, other: &Self) -> bool {
35        match (self.division(), other.division()) {
36            (Ok(lhs), Ok(rhs)) => lhs == rhs,
37            _ => false,
38        }
39    }
40}
41
42impl std::fmt::Debug for SamplingConfig {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            SamplingConfig::Division(div) => write!(f, "SamplingConfig::Division({})", div),
46            SamplingConfig::Freq(freq) => write!(f, "SamplingConfig::Freq({:?})", freq),
47            #[cfg(not(feature = "dynamic_freq"))]
48            SamplingConfig::Period(period) => write!(f, "SamplingConfig::Period({:?})", period),
49            SamplingConfig::FreqNearest(nearest) => {
50                write!(f, "SamplingConfig::FreqNearest({:?})", nearest)
51            }
52            #[cfg(not(feature = "dynamic_freq"))]
53            SamplingConfig::PeriodNearest(nearest) => {
54                write!(f, "SamplingConfig::PeriodNearest({:?})", nearest)
55            }
56        }
57    }
58}
59
60impl From<NonZeroU16> for SamplingConfig {
61    fn from(value: NonZeroU16) -> Self {
62        Self::Division(value)
63    }
64}
65
66impl From<Freq<f32>> for SamplingConfig {
67    fn from(value: Freq<f32>) -> Self {
68        Self::Freq(value)
69    }
70}
71
72#[cfg(not(feature = "dynamic_freq"))]
73impl From<std::time::Duration> for SamplingConfig {
74    fn from(value: std::time::Duration) -> Self {
75        Self::Period(value)
76    }
77}
78
79impl SamplingConfig {
80    /// A [`SamplingConfig`] of 40kHz.
81    pub const FREQ_40K: Self = SamplingConfig::Freq(Freq { freq: 40000. });
82    /// A [`SamplingConfig`] of 4kHz.
83    pub const FREQ_4K: Self = SamplingConfig::Freq(Freq { freq: 4000. });
84
85    /// Creates a new [`SamplingConfig`].
86    #[must_use]
87    pub fn new(value: impl Into<SamplingConfig>) -> Self {
88        value.into()
89    }
90
91    /// The division number of the sampling frequency.
92    ///
93    /// The sampling frequency is [`ultrasound_freq`] / `division`.
94    pub fn division(&self) -> Result<u16, SamplingConfigError> {
95        match *self {
96            SamplingConfig::Division(div) => Ok(div.get()),
97            SamplingConfig::Freq(freq) => {
98                let freq_max = ultrasound_freq().hz() as f32 * Hz;
99                let freq_min = freq_max / u16::MAX as f32;
100                if !(freq_min..=freq_max).contains(&freq) {
101                    return Err(SamplingConfigError::FreqOutOfRangeF(
102                        freq, freq_min, freq_max,
103                    ));
104                }
105                let division = ultrasound_freq().hz() as f32 / freq.hz();
106                if !is_integer(division as _) {
107                    return Err(SamplingConfigError::FreqInvalidF(freq));
108                }
109                Ok(division as _)
110            }
111            #[cfg(not(feature = "dynamic_freq"))]
112            SamplingConfig::Period(duration) => {
113                use crate::defined::ultrasound_period;
114
115                let period_min = ultrasound_period();
116                let period_max = std::time::Duration::from_micros(
117                    u16::MAX as u64 * ultrasound_period().as_micros() as u64,
118                );
119                if !(period_min..=period_max).contains(&duration) {
120                    return Err(SamplingConfigError::PeriodOutOfRange(
121                        duration, period_min, period_max,
122                    ));
123                }
124                if duration.as_nanos() % ultrasound_period().as_nanos() != 0 {
125                    return Err(SamplingConfigError::PeriodInvalid(duration));
126                }
127                Ok((duration.as_nanos() / ultrasound_period().as_nanos()) as _)
128            }
129            SamplingConfig::FreqNearest(nearest) => Ok((ultrasound_freq().hz() as f32
130                / nearest.0.hz())
131            .clamp(1.0, u16::MAX as f32)
132            .round() as u16),
133            #[cfg(not(feature = "dynamic_freq"))]
134            SamplingConfig::PeriodNearest(nearest) => {
135                use crate::defined::ultrasound_period;
136
137                Ok(((nearest.0.as_nanos() + ultrasound_period().as_nanos() / 2)
138                    / ultrasound_period().as_nanos())
139                .clamp(1, u16::MAX as u128) as u16)
140            }
141        }
142    }
143
144    /// The sampling frequency.
145    pub fn freq(&self) -> Result<Freq<f32>, SamplingConfigError> {
146        Ok(ultrasound_freq().hz() as f32 / self.division()? as f32 * Hz)
147    }
148
149    #[cfg(not(feature = "dynamic_freq"))]
150    /// The sampling period.
151    pub fn period(&self) -> Result<std::time::Duration, SamplingConfigError> {
152        Ok(crate::defined::ultrasound_period() * self.division()? as u32)
153    }
154}
155
156impl SamplingConfig {
157    /// Converts to a [`SamplingConfig`] with the nearest frequency or period among the possible values.
158    #[must_use]
159    pub const fn into_nearest(self) -> SamplingConfig {
160        match self {
161            SamplingConfig::Freq(freq) => SamplingConfig::FreqNearest(Nearest(freq)),
162            #[cfg(not(feature = "dynamic_freq"))]
163            SamplingConfig::Period(period) => SamplingConfig::PeriodNearest(Nearest(period)),
164            _ => self,
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use crate::defined::{Hz, kHz};
172
173    #[cfg(not(feature = "dynamic_freq"))]
174    use crate::defined::ultrasound_period;
175    #[cfg(not(feature = "dynamic_freq"))]
176    use std::time::Duration;
177
178    use super::*;
179
180    #[rstest::rstest]
181    #[test]
182    #[case(Ok(1), NonZeroU16::MIN)]
183    #[case(Ok(u16::MAX), NonZeroU16::MAX)]
184    #[case(Ok(1), 40000. * Hz)]
185    #[case(Ok(10), 4000. * Hz)]
186    #[case(Err(SamplingConfigError::FreqInvalidF((ultrasound_freq().hz() as f32 - 1.) * Hz)), (ultrasound_freq().hz() as f32 - 1.) * Hz)]
187    #[case(Err(SamplingConfigError::FreqOutOfRangeF(0. * Hz, ultrasound_freq().hz() as f32 * Hz / u16::MAX as f32, ultrasound_freq().hz() as f32 * Hz)), 0. * Hz)]
188    #[case(Err(SamplingConfigError::FreqOutOfRangeF(40000. * Hz + 1. * Hz, ultrasound_freq().hz() as f32 * Hz / u16::MAX as f32, ultrasound_freq().hz() as f32 * Hz)), 40000. * Hz + 1. * Hz)]
189    #[cfg_attr(not(feature = "dynamic_freq"), case(Ok(1), Duration::from_micros(25)))]
190    #[cfg_attr(
191        not(feature = "dynamic_freq"),
192        case(Ok(10), Duration::from_micros(250))
193    )]
194    #[cfg_attr(not(feature = "dynamic_freq"), case(Err(SamplingConfigError::PeriodInvalid(Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64) - Duration::from_nanos(1))), Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64) - Duration::from_nanos(1)))]
195    #[cfg_attr(not(feature = "dynamic_freq"), case(Err(SamplingConfigError::PeriodOutOfRange(ultrasound_period() / 2, ultrasound_period(), Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64))), ultrasound_period() / 2))]
196    #[cfg_attr(not(feature = "dynamic_freq"), case(Err(SamplingConfigError::PeriodOutOfRange(Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64) * 2, ultrasound_period(), Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64))), Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64) * 2))]
197    fn division(
198        #[case] expect: Result<u16, SamplingConfigError>,
199        #[case] value: impl Into<SamplingConfig>,
200    ) {
201        assert_eq!(expect, SamplingConfig::new(value).division());
202    }
203
204    #[rstest::rstest]
205    #[test]
206    #[case(Ok(40000. * Hz), NonZeroU16::MIN)]
207    #[case(Ok(0.61036086 * Hz), NonZeroU16::MAX)]
208    #[case(Ok(40000. * Hz), 40000. * Hz)]
209    #[case(Ok(4000. * Hz), 4000. * Hz)]
210    #[cfg_attr(not(feature = "dynamic_freq"), case(Ok(40000. * Hz), Duration::from_micros(25)))]
211    #[cfg_attr(not(feature = "dynamic_freq"), case(Ok(4000. * Hz), Duration::from_micros(250)))]
212    fn freq(
213        #[case] expect: Result<Freq<f32>, SamplingConfigError>,
214        #[case] value: impl Into<SamplingConfig>,
215    ) {
216        assert_eq!(expect, SamplingConfig::new(value).freq());
217    }
218
219    #[cfg(not(feature = "dynamic_freq"))]
220    #[rstest::rstest]
221    #[test]
222    #[case(Ok(Duration::from_micros(25)), NonZeroU16::MIN)]
223    #[case(Ok(Duration::from_micros(1638375)), NonZeroU16::MAX)]
224    #[case(Ok(Duration::from_micros(25)), 40000. * Hz)]
225    #[case(Ok(Duration::from_micros(250)), 4000. * Hz)]
226    #[case(Ok(Duration::from_micros(25)), Duration::from_micros(25))]
227    #[case(Ok(Duration::from_micros(250)), Duration::from_micros(250))]
228    fn period(
229        #[case] expect: Result<Duration, SamplingConfigError>,
230        #[case] value: impl Into<SamplingConfig>,
231    ) {
232        assert_eq!(expect, SamplingConfig::new(value).period());
233    }
234
235    #[rstest::rstest]
236    #[test]
237    #[case::min(u16::MAX, (40000. / u16::MAX as f32) * Hz)]
238    #[case::max(1, 40000. * Hz)]
239    #[case::not_supported_max(1, (ultrasound_freq().hz() as f32 - 1.) * Hz)]
240    #[case::out_of_range_min(u16::MAX, 0. * Hz)]
241    #[case::out_of_range_max(1, 40000. * Hz + 1. * Hz)]
242    fn from_freq_nearest(#[case] expected: u16, #[case] freq: Freq<f32>) {
243        assert_eq!(
244            Ok(expected),
245            SamplingConfig::new(freq).into_nearest().division()
246        );
247    }
248
249    #[cfg(not(feature = "dynamic_freq"))]
250    #[rstest::rstest]
251    #[test]
252    #[case::min(1, ultrasound_period())]
253    #[case::max(u16::MAX, Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64))]
254    #[case::not_supported_max(u16::MAX, Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64) - Duration::from_nanos(1))]
255    #[case::out_of_range_min(1, ultrasound_period() / 2)]
256    #[case::out_of_range_max(u16::MAX, Duration::from_micros(u16::MAX as u64 * ultrasound_period().as_micros() as u64) * 2)]
257    fn from_period_nearest(#[case] expected: u16, #[case] p: Duration) {
258        assert_eq!(
259            Ok(expected),
260            SamplingConfig::new(p).into_nearest().division()
261        );
262    }
263
264    #[rstest::rstest]
265    #[case(
266        SamplingConfig::Division(NonZeroU16::MIN),
267        SamplingConfig::Division(NonZeroU16::MIN)
268    )]
269    #[case(SamplingConfig::FreqNearest(Nearest(1. * Hz)), SamplingConfig::Freq(1. * Hz))]
270    #[cfg_attr(
271        not(feature = "dynamic_freq"),
272        case(
273            SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1))),
274            SamplingConfig::Period(Duration::from_micros(1))
275        )
276    )]
277    #[case(SamplingConfig::FreqNearest(Nearest(1. * Hz)), SamplingConfig::FreqNearest(Nearest(1. * Hz)))]
278    #[cfg_attr(
279        not(feature = "dynamic_freq"),
280        case(
281            SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1))),
282            SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1)))
283        )
284    )]
285    #[test]
286    fn into_nearest(#[case] expect: SamplingConfig, #[case] config: SamplingConfig) {
287        assert_eq!(expect, config.into_nearest());
288    }
289
290    #[rstest::rstest]
291    #[case(true, SamplingConfig::FREQ_40K, SamplingConfig::FREQ_40K)]
292    #[case(true, SamplingConfig::FREQ_40K, SamplingConfig::new(NonZeroU16::MIN))]
293    #[case(true, SamplingConfig::FREQ_40K, SamplingConfig::new(40. * kHz))]
294    #[cfg_attr(
295        not(feature = "dynamic_freq"),
296        case(
297            true,
298            SamplingConfig::FREQ_40K,
299            SamplingConfig::new(std::time::Duration::from_micros(25))
300        )
301    )]
302    #[case(false, SamplingConfig::new(41. * kHz), SamplingConfig::new(41. * kHz))]
303    #[test]
304    fn partial_eq(#[case] expect: bool, #[case] lhs: SamplingConfig, #[case] rhs: SamplingConfig) {
305        assert_eq!(expect, lhs == rhs);
306    }
307
308    #[rstest::rstest]
309    #[case(
310        "SamplingConfig::Division(1)",
311        SamplingConfig::Division(NonZeroU16::MIN)
312    )]
313    #[case("SamplingConfig::Freq(1 Hz)", SamplingConfig::Freq(1. * Hz))]
314    #[cfg_attr(
315        not(feature = "dynamic_freq"),
316        case(
317            "SamplingConfig::Period(1µs)",
318            SamplingConfig::Period(Duration::from_micros(1))
319        )
320    )]
321    #[case("SamplingConfig::FreqNearest(Nearest(1 Hz))", SamplingConfig::FreqNearest(Nearest(1. * Hz)))]
322    #[cfg_attr(
323        not(feature = "dynamic_freq"),
324        case(
325            "SamplingConfig::PeriodNearest(Nearest(1µs))",
326            SamplingConfig::PeriodNearest(Nearest(Duration::from_micros(1)))
327        )
328    )]
329    #[test]
330    fn debug(#[case] expect: &str, #[case] config: SamplingConfig) {
331        assert_eq!(expect, format!("{:?}", config));
332    }
333}