Skip to main content

dioxus_audio/
waveform.rs

1//! Immutable, duration-aware Waveform Data values.
2
3use 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/// An observable, source-time Waveform Viewport and its navigation commands.
16///
17/// Create this Controller with [`use_waveform_viewport`]. Pan distances are
18/// expressed as fractions of the current visible span. Zoom factors greater
19/// than one zoom in, and the supplied source-time anchor remains at the same
20/// relative position whenever source boundaries permit it.
21#[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    /// The complete source duration.
35    pub fn total_duration(self) -> Duration {
36        self.state.read().total_duration
37    }
38
39    /// The current positive, half-open visible source-time interval.
40    pub fn visible_range(self) -> Range<Duration> {
41        self.state.read().visible.clone()
42    }
43
44    /// Whether Playback position updates currently move the Viewport.
45    pub fn is_following(self) -> bool {
46        self.state.read().following
47    }
48
49    /// Keep Playback within the Viewport's forward safe zone while following.
50    ///
51    /// Positions inside the 15–75% safe zone do not move the Viewport. Once a
52    /// position leaves it, the Viewport moves to place Playback at 25% unless a
53    /// source boundary intervenes. Positions beyond the source are clamped.
54    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    /// Resume following Playback and immediately reveal its current position.
84    pub fn resume_follow(mut self, position: Duration) {
85        self.state.write().following = true;
86        self.follow_playback(position);
87    }
88
89    /// Move by a fraction of the current visible span.
90    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    /// Present a requested source-time interval after clamping it to the source.
114    ///
115    /// A collapsed or reversed request becomes the smallest representable
116    /// positive interval at its requested start.
117    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    /// Restore the complete source duration.
124    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    /// Zoom by `factor` around an explicit source-time anchor.
131    ///
132    /// Invalid or non-positive factors leave the Viewport unchanged. Extreme
133    /// factors clamp between one nanosecond and the complete source duration.
134    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
157/// Create a Waveform Viewport Controller with hydration-stable initial state.
158///
159/// `total_duration` must be positive. The optional initial range is clamped to
160/// that duration; omitting it presents the complete source.
161pub 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/// The amplitude interpretation shared by every resolution in Waveform Data.
200#[derive(Clone, Copy, Debug, PartialEq, Eq)]
201pub enum AmplitudeMode {
202    Magnitude,
203    SignedEnvelope,
204}
205
206/// One minimum and maximum signed amplitude pair.
207#[derive(Clone, Copy, Debug, PartialEq)]
208pub struct SignedEnvelope {
209    /// The minimum observed signed amplitude.
210    pub min: f32,
211    /// The maximum observed signed amplitude.
212    pub max: f32,
213}
214
215/// One source-time resolution supplied when constructing Waveform Data.
216#[derive(Clone, Debug, PartialEq)]
217pub struct WaveformLevel<T> {
218    bucket_span: Duration,
219    buckets: Vec<T>,
220}
221
222impl<T> WaveformLevel<T> {
223    /// Create a level from a positive span and flat channel-major buckets.
224    pub fn new(bucket_span: Duration, buckets: Vec<T>) -> Self {
225        Self {
226            bucket_span,
227            buckets,
228        }
229    }
230}
231
232/// An exact positive source-time bucket span expressed as a duration ratio.
233///
234/// Explicit levels use a divisor of one. Peaks conversion may need a larger
235/// divisor when its evenly spaced span is not an integral number of nanoseconds.
236#[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    /// Return the span as a `Duration` when it is an integral number of
266    /// nanoseconds.
267    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/// A Waveform Data construction or selection error.
277#[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/// An immutable Waveform Data snapshot.
399///
400/// Clones share storage and compare by snapshot identity. Independently
401/// constructed values compare unequal even when their buckets match.
402#[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/// Metadata for one Waveform Data resolution.
436#[derive(Clone, Copy, Debug, PartialEq, Eq)]
437pub struct ResolutionInfo {
438    bucket_span: WaveformBucketSpan,
439    buckets_per_channel: usize,
440}
441
442impl ResolutionInfo {
443    /// The exact source-time span of each full bucket.
444    pub fn bucket_span(self) -> WaveformBucketSpan {
445        self.bucket_span
446    }
447
448    /// The number of buckets stored for each channel.
449    pub fn buckets_per_channel(self) -> usize {
450        self.buckets_per_channel
451    }
452}
453
454/// A borrowed amplitude slice from one channel in a selected resolution.
455#[derive(Clone, Copy, Debug, PartialEq)]
456pub enum AmplitudeSlice<'a> {
457    /// Borrowed normalized Magnitude buckets.
458    Magnitudes(&'a [f32]),
459    /// Borrowed Signed Envelope buckets.
460    SignedEnvelopes(&'a [SignedEnvelope]),
461}
462
463/// A borrowed range from one Waveform Data resolution.
464#[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    /// Construct Magnitude Waveform Data from normalized channel-major levels.
480    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    /// Construct Signed Envelope Waveform Data from channel-major levels.
508    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    /// Convert Peaks into one evenly spaced mono Magnitude resolution.
542    ///
543    /// Peaks do not carry their original cadence, channel structure, or sign,
544    /// so conversion cannot preserve those facts.
545    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    /// Borrow the finest resolution fitting a per-channel bucket budget.
609    ///
610    /// `range` is a nonempty half-open source-time range. The coarsest
611    /// resolution is returned when no stored resolution fits the budget.
612    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    /// The selected resolution's finest-to-coarsest index.
742    pub fn resolution_index(&self) -> usize {
743        self.resolution
744    }
745
746    /// The exact source-time span of each full bucket.
747    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    /// The index of the first intersecting bucket in the full resolution.
755    pub fn first_bucket(&self) -> usize {
756        self.buckets.start
757    }
758
759    /// The number of intersecting buckets per channel.
760    pub fn bucket_count(&self) -> usize {
761        self.buckets.len()
762    }
763
764    /// Borrow one channel's selected buckets without copying them.
765    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}