1use std::fmt;
4use std::ops::Range;
5use std::sync::Arc;
6use std::time::Duration;
7
8use dioxus::prelude::*;
9
10const MINIMUM_VIEWPORT_SPAN: Duration = Duration::from_nanos(1);
11const FOLLOW_SAFE_ZONE_START_PERCENT: u128 = 15;
12const FOLLOW_SAFE_ZONE_END_PERCENT: u128 = 75;
13const FOLLOW_TARGET_PERCENT: u128 = 25;
14
15#[derive(Clone, Copy, PartialEq)]
22pub struct WaveformViewportController {
23 state: Signal<WaveformViewportState>,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
27struct WaveformViewportState {
28 total_duration: Duration,
29 visible: Range<Duration>,
30 following: bool,
31}
32
33impl WaveformViewportController {
34 pub fn total_duration(self) -> Duration {
36 self.state.read().total_duration
37 }
38
39 pub fn visible_range(self) -> Range<Duration> {
41 self.state.read().visible.clone()
42 }
43
44 pub fn is_following(self) -> bool {
46 self.state.read().following
47 }
48
49 pub fn follow_playback(mut self, position: Duration) {
55 let state = self.state.read();
56 if !state.following {
57 return;
58 }
59 let visible = state.visible.clone();
60 let total_duration = state.total_duration;
61 drop(state);
62
63 let span_nanos = (visible.end - visible.start).as_nanos();
64 let start_nanos = visible.start.as_nanos();
65 let position_nanos = position.min(total_duration).as_nanos();
66 let safe_start = start_nanos + scaled_nanos(span_nanos, FOLLOW_SAFE_ZONE_START_PERCENT);
67 let safe_end = start_nanos + scaled_nanos(span_nanos, FOLLOW_SAFE_ZONE_END_PERCENT);
68 if (safe_start..=safe_end).contains(&position_nanos) {
69 return;
70 }
71
72 let target_offset = scaled_nanos(span_nanos, FOLLOW_TARGET_PERCENT);
73 let maximum_start = (total_duration - (visible.end - visible.start)).as_nanos();
74 let next_start = position_nanos
75 .saturating_sub(target_offset)
76 .min(maximum_start);
77 if next_start != start_nanos {
78 let next_start = duration_from_nanos(next_start);
79 self.state.write().visible = next_start..next_start + (visible.end - visible.start);
80 }
81 }
82
83 pub fn resume_follow(mut self, position: Duration) {
85 self.state.write().following = true;
86 self.follow_playback(position);
87 }
88
89 pub fn pan(mut self, fraction: f64) {
91 if !fraction.is_finite() {
92 return;
93 }
94
95 let mut state = self.state.write();
96 state.following = false;
97 let span = state.visible.end - state.visible.start;
98 let current_start = state.visible.start.as_nanos();
99 let maximum_start = (state.total_duration - span).as_nanos();
100 let distance = span.as_nanos() as f64 * fraction.abs();
101 let start_nanos = if fraction.is_sign_negative() {
102 current_start.saturating_sub(rounded_nanos_clamped(distance, current_start))
103 } else {
104 current_start.saturating_add(rounded_nanos_clamped(
105 distance,
106 maximum_start - current_start,
107 ))
108 };
109 let start = duration_from_nanos(start_nanos);
110 state.visible = start..start + span;
111 }
112
113 pub fn show_range(mut self, range: Range<Duration>) {
118 let mut state = self.state.write();
119 state.following = false;
120 state.visible = clamp_viewport_range(state.total_duration, range);
121 }
122
123 pub fn reset(mut self) {
125 let mut state = self.state.write();
126 state.following = false;
127 state.visible = Duration::ZERO..state.total_duration;
128 }
129
130 pub fn zoom(mut self, factor: f64, anchor: Duration) {
135 if !factor.is_finite() || factor <= 0.0 {
136 return;
137 }
138
139 let mut state = self.state.write();
140 state.following = false;
141 let old_start = state.visible.start.as_nanos();
142 let old_end = state.visible.end.as_nanos();
143 let old_span = old_end - old_start;
144 let anchor = anchor.as_nanos().clamp(old_start, old_end);
145 let anchor_fraction = (anchor - old_start) as f64 / old_span as f64;
146 let total_nanos = state.total_duration.as_nanos();
147 let new_span_nanos = rounded_nanos_clamped(old_span as f64 / factor, total_nanos).max(1);
148 let maximum_start = total_nanos - new_span_nanos;
149 let new_anchor_offset =
150 rounded_nanos_clamped(new_span_nanos as f64 * anchor_fraction, new_span_nanos);
151 let new_start_nanos = anchor.saturating_sub(new_anchor_offset).min(maximum_start);
152 state.visible = duration_from_nanos(new_start_nanos)
153 ..duration_from_nanos(new_start_nanos + new_span_nanos);
154 }
155}
156
157pub fn use_waveform_viewport(
162 total_duration: Duration,
163 initial_visible: Option<Range<Duration>>,
164) -> WaveformViewportController {
165 assert!(
166 !total_duration.is_zero(),
167 "Waveform Viewport duration must be positive"
168 );
169 let state = use_signal(move || WaveformViewportState {
170 total_duration,
171 visible: initial_visible
172 .map(|range| clamp_viewport_range(total_duration, range))
173 .unwrap_or(Duration::ZERO..total_duration),
174 following: true,
175 });
176 WaveformViewportController { state }
177}
178
179fn clamp_viewport_range(total_duration: Duration, range: Range<Duration>) -> Range<Duration> {
180 let requested_span = range
181 .end
182 .checked_sub(range.start)
183 .filter(|span| !span.is_zero())
184 .unwrap_or(MINIMUM_VIEWPORT_SPAN);
185 let span = requested_span.min(total_duration);
186 let maximum_start = total_duration - span;
187 let start = range.start.min(maximum_start);
188 start..start + span
189}
190
191fn rounded_nanos_clamped(nanos: f64, maximum: u128) -> u128 {
192 (nanos.round().clamp(0.0, maximum as f64) as u128).min(maximum)
193}
194
195fn scaled_nanos(nanos: u128, percent: u128) -> u128 {
196 nanos / 100 * percent + nanos % 100 * percent / 100
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub enum AmplitudeMode {
202 Magnitude,
203 SignedEnvelope,
204}
205
206#[derive(Clone, Copy, Debug, PartialEq)]
208pub struct SignedEnvelope {
209 pub min: f32,
211 pub max: f32,
213}
214
215#[derive(Clone, Debug, PartialEq)]
217pub struct WaveformLevel<T> {
218 bucket_span: Duration,
219 buckets: Vec<T>,
220}
221
222impl<T> WaveformLevel<T> {
223 pub fn new(bucket_span: Duration, buckets: Vec<T>) -> Self {
225 Self {
226 bucket_span,
227 buckets,
228 }
229 }
230}
231
232#[derive(Clone, Copy, Debug, PartialEq, Eq)]
237pub struct WaveformBucketSpan {
238 numerator: Duration,
239 divisor: usize,
240}
241
242impl WaveformBucketSpan {
243 fn from_duration(duration: Duration) -> Self {
244 Self {
245 numerator: duration,
246 divisor: 1,
247 }
248 }
249
250 fn evenly_spaced(duration: Duration, buckets: usize) -> Self {
251 Self {
252 numerator: duration,
253 divisor: buckets,
254 }
255 }
256
257 pub fn numerator(self) -> Duration {
258 self.numerator
259 }
260
261 pub fn divisor(self) -> usize {
262 self.divisor
263 }
264
265 pub fn exact_duration(self) -> Option<Duration> {
268 let divisor = self.divisor as u128;
269 let nanos = self.numerator.as_nanos();
270 nanos
271 .is_multiple_of(divisor)
272 .then(|| duration_from_nanos(nanos / divisor))
273 }
274}
275
276#[derive(Clone, Debug, PartialEq)]
278#[non_exhaustive]
279pub enum WaveformError {
280 ZeroDuration,
281 NoChannels,
282 NoResolutions,
283 ZeroBucketSpan {
284 resolution: usize,
285 },
286 NonIncreasingBucketSpan {
287 resolution: usize,
288 },
289 NoBuckets {
290 resolution: usize,
291 },
292 MisalignedChannelData {
293 resolution: usize,
294 values: usize,
295 channels: usize,
296 },
297 BucketCountOverflow {
298 resolution: usize,
299 },
300 DurationCoverage {
301 resolution: usize,
302 expected_buckets: usize,
303 actual_buckets: usize,
304 },
305 InvalidMagnitude {
306 resolution: usize,
307 channel: usize,
308 bucket: usize,
309 value: f32,
310 },
311 InvalidSignedEnvelope {
312 resolution: usize,
313 channel: usize,
314 bucket: usize,
315 min: f32,
316 max: f32,
317 },
318 ZeroBucketBudget,
319 InvalidRange {
320 start: Duration,
321 end: Duration,
322 duration: Duration,
323 },
324 EmptyPeaks,
325}
326
327impl fmt::Display for WaveformError {
328 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
329 match self {
330 Self::ZeroDuration => formatter.write_str("duration must be positive"),
331 Self::NoChannels => formatter.write_str("channel count must be positive"),
332 Self::NoResolutions => formatter.write_str("at least one resolution is required"),
333 Self::ZeroBucketSpan { resolution } => {
334 write!(formatter, "resolution {resolution} has a zero bucket span")
335 }
336 Self::NonIncreasingBucketSpan { resolution } => write!(
337 formatter,
338 "resolution {resolution} is not ordered strictly coarser than its predecessor"
339 ),
340 Self::NoBuckets { resolution } => {
341 write!(formatter, "resolution {resolution} has no buckets")
342 }
343 Self::MisalignedChannelData {
344 resolution,
345 values,
346 channels,
347 } => write!(
348 formatter,
349 "resolution {resolution} has {values} values, which cannot form {channels} equal channels"
350 ),
351 Self::BucketCountOverflow { resolution } => write!(
352 formatter,
353 "resolution {resolution} requires more buckets than this target can address"
354 ),
355 Self::DurationCoverage {
356 resolution,
357 expected_buckets,
358 actual_buckets,
359 } => write!(
360 formatter,
361 "resolution {resolution} needs {expected_buckets} buckets per channel to cover the duration, but has {actual_buckets}"
362 ),
363 Self::InvalidMagnitude {
364 resolution,
365 channel,
366 bucket,
367 value,
368 } => write!(
369 formatter,
370 "resolution {resolution}, channel {channel}, bucket {bucket} has invalid magnitude {value:?}"
371 ),
372 Self::InvalidSignedEnvelope {
373 resolution,
374 channel,
375 bucket,
376 min,
377 max,
378 } => write!(
379 formatter,
380 "resolution {resolution}, channel {channel}, bucket {bucket} has invalid signed envelope [{min:?}, {max:?}]"
381 ),
382 Self::ZeroBucketBudget => formatter.write_str("bucket budget must be positive"),
383 Self::InvalidRange {
384 start,
385 end,
386 duration,
387 } => write!(
388 formatter,
389 "range {start:?}..{end:?} must satisfy start < end <= {duration:?}"
390 ),
391 Self::EmptyPeaks => formatter.write_str("Peaks must be nonempty"),
392 }
393 }
394}
395
396impl std::error::Error for WaveformError {}
397
398#[derive(Clone, Debug)]
403pub struct WaveformData {
404 inner: Arc<WaveformDataInner>,
405}
406
407impl PartialEq for WaveformData {
408 fn eq(&self, other: &Self) -> bool {
409 Arc::ptr_eq(&self.inner, &other.inner)
410 }
411}
412
413impl Eq for WaveformData {}
414
415#[derive(Debug)]
416struct WaveformDataInner {
417 duration: Duration,
418 channels: usize,
419 ladder: AmplitudeLadder,
420}
421
422#[derive(Debug)]
423enum AmplitudeLadder {
424 Magnitude(Vec<Resolution<f32>>),
425 SignedEnvelope(Vec<Resolution<SignedEnvelope>>),
426}
427
428#[derive(Debug)]
429struct Resolution<T> {
430 bucket_span: WaveformBucketSpan,
431 buckets_per_channel: usize,
432 buckets: Vec<T>,
433}
434
435#[derive(Clone, Copy, Debug, PartialEq, Eq)]
437pub struct ResolutionInfo {
438 bucket_span: WaveformBucketSpan,
439 buckets_per_channel: usize,
440}
441
442impl ResolutionInfo {
443 pub fn bucket_span(self) -> WaveformBucketSpan {
445 self.bucket_span
446 }
447
448 pub fn buckets_per_channel(self) -> usize {
450 self.buckets_per_channel
451 }
452}
453
454#[derive(Clone, Copy, Debug, PartialEq)]
456pub enum AmplitudeSlice<'a> {
457 Magnitudes(&'a [f32]),
459 SignedEnvelopes(&'a [SignedEnvelope]),
461}
462
463#[derive(Debug)]
465pub struct WaveformView<'a> {
466 resolution: usize,
467 channels: usize,
468 buckets: Range<usize>,
469 level: ViewLevel<'a>,
470}
471
472#[derive(Debug)]
473enum ViewLevel<'a> {
474 Magnitude(&'a Resolution<f32>),
475 SignedEnvelope(&'a Resolution<SignedEnvelope>),
476}
477
478impl WaveformData {
479 pub fn from_magnitudes(
481 duration: Duration,
482 channels: usize,
483 levels: Vec<WaveformLevel<f32>>,
484 ) -> Result<Self, WaveformError> {
485 let validated = validate_levels(duration, channels, levels, |location, value| {
486 if value.is_finite() && (0.0..=1.0).contains(value) {
487 Ok(())
488 } else {
489 Err(WaveformError::InvalidMagnitude {
490 resolution: location.resolution,
491 channel: location.channel,
492 bucket: location.bucket,
493 value: *value,
494 })
495 }
496 })?;
497
498 Ok(Self {
499 inner: Arc::new(WaveformDataInner {
500 duration,
501 channels,
502 ladder: AmplitudeLadder::Magnitude(validated),
503 }),
504 })
505 }
506
507 pub fn from_signed_envelopes(
509 duration: Duration,
510 channels: usize,
511 levels: Vec<WaveformLevel<SignedEnvelope>>,
512 ) -> Result<Self, WaveformError> {
513 let validated = validate_levels(duration, channels, levels, |location, value| {
514 if value.min.is_finite()
515 && value.max.is_finite()
516 && (-1.0..=1.0).contains(&value.min)
517 && (-1.0..=1.0).contains(&value.max)
518 && value.min <= value.max
519 {
520 Ok(())
521 } else {
522 Err(WaveformError::InvalidSignedEnvelope {
523 resolution: location.resolution,
524 channel: location.channel,
525 bucket: location.bucket,
526 min: value.min,
527 max: value.max,
528 })
529 }
530 })?;
531
532 Ok(Self {
533 inner: Arc::new(WaveformDataInner {
534 duration,
535 channels,
536 ladder: AmplitudeLadder::SignedEnvelope(validated),
537 }),
538 })
539 }
540
541 pub fn from_peaks(duration: Duration, peaks: Vec<u8>) -> Result<Self, WaveformError> {
546 if duration.is_zero() {
547 return Err(WaveformError::ZeroDuration);
548 }
549 if peaks.is_empty() {
550 return Err(WaveformError::EmptyPeaks);
551 }
552
553 let buckets_per_channel = peaks.len();
554 let buckets = peaks
555 .into_iter()
556 .map(|peak| f32::from(peak) / 255.0)
557 .collect();
558 Ok(Self {
559 inner: Arc::new(WaveformDataInner {
560 duration,
561 channels: 1,
562 ladder: AmplitudeLadder::Magnitude(vec![Resolution {
563 bucket_span: WaveformBucketSpan::evenly_spaced(duration, buckets_per_channel),
564 buckets_per_channel,
565 buckets,
566 }]),
567 }),
568 })
569 }
570
571 pub fn mode(&self) -> AmplitudeMode {
572 match self.inner.ladder {
573 AmplitudeLadder::Magnitude(_) => AmplitudeMode::Magnitude,
574 AmplitudeLadder::SignedEnvelope(_) => AmplitudeMode::SignedEnvelope,
575 }
576 }
577
578 pub fn duration(&self) -> Duration {
579 self.inner.duration
580 }
581
582 pub fn channel_count(&self) -> usize {
583 self.inner.channels
584 }
585
586 pub fn resolution_count(&self) -> usize {
587 match &self.inner.ladder {
588 AmplitudeLadder::Magnitude(levels) => levels.len(),
589 AmplitudeLadder::SignedEnvelope(levels) => levels.len(),
590 }
591 }
592
593 pub fn resolution(&self, index: usize) -> Option<ResolutionInfo> {
594 let level = match &self.inner.ladder {
595 AmplitudeLadder::Magnitude(levels) => levels
596 .get(index)
597 .map(|level| (level.bucket_span, level.buckets_per_channel)),
598 AmplitudeLadder::SignedEnvelope(levels) => levels
599 .get(index)
600 .map(|level| (level.bucket_span, level.buckets_per_channel)),
601 }?;
602 Some(ResolutionInfo {
603 bucket_span: level.0,
604 buckets_per_channel: level.1,
605 })
606 }
607
608 pub fn select(
613 &self,
614 range: Range<Duration>,
615 bucket_budget: usize,
616 ) -> Result<WaveformView<'_>, WaveformError> {
617 if bucket_budget == 0 {
618 return Err(WaveformError::ZeroBucketBudget);
619 }
620 if range.start >= range.end || range.end > self.duration() {
621 return Err(WaveformError::InvalidRange {
622 start: range.start,
623 end: range.end,
624 duration: self.duration(),
625 });
626 }
627
628 let mut chosen = self.resolution_count() - 1;
629 let mut chosen_range = 0..0;
630 for index in 0..self.resolution_count() {
631 let resolution = self.resolution(index).expect("validated resolution");
632 let range = intersecting_buckets(
633 range.clone(),
634 resolution.bucket_span,
635 resolution.buckets_per_channel,
636 );
637 chosen = index;
638 chosen_range = range.clone();
639 if range.len() <= bucket_budget {
640 break;
641 }
642 }
643
644 let level = match &self.inner.ladder {
645 AmplitudeLadder::Magnitude(levels) => ViewLevel::Magnitude(&levels[chosen]),
646 AmplitudeLadder::SignedEnvelope(levels) => ViewLevel::SignedEnvelope(&levels[chosen]),
647 };
648 Ok(WaveformView {
649 resolution: chosen,
650 channels: self.channel_count(),
651 buckets: chosen_range,
652 level,
653 })
654 }
655}
656
657fn bucket_count(duration: Duration, bucket_span: Duration) -> Option<usize> {
658 let count = duration.as_nanos().div_ceil(bucket_span.as_nanos());
659 usize::try_from(count).ok()
660}
661
662fn intersecting_buckets(
663 range: Range<Duration>,
664 bucket_span: WaveformBucketSpan,
665 buckets_per_channel: usize,
666) -> Range<usize> {
667 let numerator = bucket_span.numerator.as_nanos();
668 let (start, _) = multiply_divide(range.start.as_nanos(), bucket_span.divisor, numerator)
669 .unwrap_or((buckets_per_channel, false));
670 let (end_floor, end_remainder) =
671 multiply_divide(range.end.as_nanos(), bucket_span.divisor, numerator)
672 .unwrap_or((buckets_per_channel, false));
673 let end = end_floor.saturating_add(usize::from(end_remainder));
674 let start = start.min(buckets_per_channel);
675 let end = end.min(buckets_per_channel);
676 start..end
677}
678
679fn multiply_divide(value: u128, multiplier: usize, divisor: u128) -> Option<(usize, bool)> {
680 let mut multiplier = multiplier;
681 let mut factor_whole = value / divisor;
682 let mut factor_remainder = value % divisor;
683 let mut result_whole = 0_u128;
684 let mut result_remainder = 0_u128;
685
686 while multiplier > 0 {
687 if multiplier & 1 == 1 {
688 add_fraction(
689 &mut result_whole,
690 &mut result_remainder,
691 factor_whole,
692 factor_remainder,
693 divisor,
694 )?;
695 }
696
697 multiplier >>= 1;
698 if multiplier > 0 {
699 let add_whole = factor_whole;
700 let add_remainder = factor_remainder;
701 add_fraction(
702 &mut factor_whole,
703 &mut factor_remainder,
704 add_whole,
705 add_remainder,
706 divisor,
707 )?;
708 }
709 }
710
711 Some((usize::try_from(result_whole).ok()?, result_remainder != 0))
712}
713
714fn add_fraction(
715 whole: &mut u128,
716 remainder: &mut u128,
717 add_whole: u128,
718 add_remainder: u128,
719 divisor: u128,
720) -> Option<()> {
721 *whole = whole.checked_add(add_whole)?;
722 if *remainder >= divisor - add_remainder {
723 *whole = whole.checked_add(1)?;
724 *remainder -= divisor - add_remainder;
725 } else {
726 *remainder += add_remainder;
727 }
728 Some(())
729}
730
731fn duration_from_nanos(nanos: u128) -> Duration {
732 let seconds = nanos / 1_000_000_000;
733 let subsec_nanos = (nanos % 1_000_000_000) as u32;
734 Duration::new(
735 u64::try_from(seconds).expect("a divided Duration still fits Duration"),
736 subsec_nanos,
737 )
738}
739
740impl<'a> WaveformView<'a> {
741 pub fn resolution_index(&self) -> usize {
743 self.resolution
744 }
745
746 pub fn bucket_span(&self) -> WaveformBucketSpan {
748 match self.level {
749 ViewLevel::Magnitude(level) => level.bucket_span,
750 ViewLevel::SignedEnvelope(level) => level.bucket_span,
751 }
752 }
753
754 pub fn first_bucket(&self) -> usize {
756 self.buckets.start
757 }
758
759 pub fn bucket_count(&self) -> usize {
761 self.buckets.len()
762 }
763
764 pub fn channel(&self, channel: usize) -> Option<AmplitudeSlice<'a>> {
766 if channel >= self.channels {
767 return None;
768 }
769
770 match self.level {
771 ViewLevel::Magnitude(level) => {
772 let channel_start = channel.checked_mul(level.buckets_per_channel)?;
773 let start = channel_start.checked_add(self.buckets.start)?;
774 let end = channel_start.checked_add(self.buckets.end)?;
775 Some(AmplitudeSlice::Magnitudes(level.buckets.get(start..end)?))
776 }
777 ViewLevel::SignedEnvelope(level) => {
778 let channel_start = channel.checked_mul(level.buckets_per_channel)?;
779 let start = channel_start.checked_add(self.buckets.start)?;
780 let end = channel_start.checked_add(self.buckets.end)?;
781 Some(AmplitudeSlice::SignedEnvelopes(
782 level.buckets.get(start..end)?,
783 ))
784 }
785 }
786 }
787}
788
789#[derive(Clone, Copy)]
790struct BucketLocation {
791 resolution: usize,
792 channel: usize,
793 bucket: usize,
794}
795
796fn validate_levels<T>(
797 duration: Duration,
798 channels: usize,
799 levels: Vec<WaveformLevel<T>>,
800 mut validate_bucket: impl FnMut(BucketLocation, &T) -> Result<(), WaveformError>,
801) -> Result<Vec<Resolution<T>>, WaveformError> {
802 if duration.is_zero() {
803 return Err(WaveformError::ZeroDuration);
804 }
805 if channels == 0 {
806 return Err(WaveformError::NoChannels);
807 }
808 if levels.is_empty() {
809 return Err(WaveformError::NoResolutions);
810 }
811
812 let mut previous_span = Duration::ZERO;
813 let mut validated = Vec::with_capacity(levels.len());
814 for (resolution, level) in levels.into_iter().enumerate() {
815 if level.bucket_span.is_zero() {
816 return Err(WaveformError::ZeroBucketSpan { resolution });
817 }
818 if resolution > 0 && level.bucket_span <= previous_span {
819 return Err(WaveformError::NonIncreasingBucketSpan { resolution });
820 }
821 if level.buckets.is_empty() {
822 return Err(WaveformError::NoBuckets { resolution });
823 }
824 if level.buckets.len() % channels != 0 {
825 return Err(WaveformError::MisalignedChannelData {
826 resolution,
827 values: level.buckets.len(),
828 channels,
829 });
830 }
831
832 let buckets_per_channel = level.buckets.len() / channels;
833 let expected_buckets = bucket_count(duration, level.bucket_span)
834 .ok_or(WaveformError::BucketCountOverflow { resolution })?;
835 if buckets_per_channel != expected_buckets {
836 return Err(WaveformError::DurationCoverage {
837 resolution,
838 expected_buckets,
839 actual_buckets: buckets_per_channel,
840 });
841 }
842
843 for (index, value) in level.buckets.iter().enumerate() {
844 validate_bucket(
845 BucketLocation {
846 resolution,
847 channel: index / buckets_per_channel,
848 bucket: index % buckets_per_channel,
849 },
850 value,
851 )?;
852 }
853
854 previous_span = level.bucket_span;
855 validated.push(Resolution {
856 bucket_span: WaveformBucketSpan::from_duration(level.bucket_span),
857 buckets_per_channel,
858 buckets: level.buckets,
859 });
860 }
861
862 Ok(validated)
863}