Skip to main content

dspk_view/
view.rs

1//! What a viewer asks for ([`View`]) and what it gets ([`Envelope`]), the same whether the viewer
2//! runs next to the recording or across the network (dspk-stream carries both).
3
4use std::ops::Range;
5
6use dspk_core::{DspError, DspResult, RecordingSource};
7
8use crate::envelope::fold::{finish, fold_block, Columns, EMPTY};
9use crate::pyramid::Pyramid;
10use crate::read::read_channel_blocks;
11
12/// Most values (channels × samples) one raw read of a view holds: bounds the memory of a raw
13/// window, not the work (every sample of the window is still read once).
14pub const RAW_BLOCK_VALUES: usize = 1 << 22;
15
16/// A window of a recording drawn `width` columns wide.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct View {
19    pub channels: Vec<usize>,
20    /// First sample.
21    pub start: u64,
22    /// One past the last sample.
23    pub end: u64,
24    /// Pixel columns.
25    pub width: usize,
26}
27
28/// The contents of a [`View`], row by row in the order of `View::channels`.
29#[derive(Debug, Clone, PartialEq)]
30pub enum Envelope {
31    /// Fewer samples than columns: the samples themselves (`channels × samples`), drawn as lines.
32    Samples(Vec<f32>),
33    /// `[min, max]` per column (`channels × width`); NaN where nothing is known yet or every
34    /// sample is NaN. `complete` is false while some columns wait for the pyramid to be built:
35    /// ask again later.
36    Columns { values: Vec<[f32; 2]>, complete: bool },
37}
38
39impl View {
40    /// Samples in the window.
41    pub fn samples(&self) -> u64 {
42        self.end.saturating_sub(self.start)
43    }
44
45    /// The window split evenly into `width` columns.
46    pub fn columns(&self) -> Columns {
47        Columns::Even { start: self.start, len: self.samples(), width: self.width }
48    }
49
50    /// Checks the view against a recording of `channels × samples`.
51    pub fn validate(&self, channels: usize, samples: u64) -> DspResult<()> {
52        if let Some(&channel) = self.channels.iter().find(|&&c| c >= channels) {
53            return Err(DspError::InvalidChannel { channel, total: channels });
54        }
55        if self.start >= self.end || self.end > samples || self.width == 0 {
56            return Err(DspError::SampleRange { start: self.start, end: self.end, total: samples });
57        }
58        Ok(())
59    }
60
61    /// The view's contents, as cheaply as is exact (see [`Self::read_into`]).
62    pub fn read(&self, source: &dyn RecordingSource, pyramid: Option<&Pyramid>) -> DspResult<Envelope> {
63        let mut out = Envelope::Samples(Vec::new());
64        self.read_into(source, pyramid, &mut out)?;
65        Ok(out)
66    }
67
68    /// The view's contents into `out`, reusing its buffer, as cheaply as is exact:
69    /// - fewer samples than columns: the samples, read from `source`;
70    /// - columns of at least the pyramid's base: from `pyramid` (no samples read);
71    /// - otherwise (finer columns, or no pyramid): the samples read from `source` and folded into
72    ///   columns, streamed in blocks of at most [`RAW_BLOCK_VALUES`] values aligned to the source's
73    ///   storage chunks (each chunk decoded once), the next block read while the current one is
74    ///   folded. Memory stays bounded however long the window.
75    pub fn read_into(&self, source: &dyn RecordingSource, pyramid: Option<&Pyramid>, out: &mut Envelope) -> DspResult<()> {
76        let info = source.info();
77        self.validate(info.channel_count(), info.samples)?;
78        let (rows, n) = (self.channels.len(), self.samples());
79        let (mut samples, mut values) = match std::mem::replace(out, Envelope::Samples(Vec::new())) {
80            Envelope::Samples(s) => (s, Vec::new()),
81            Envelope::Columns { values, .. } => (Vec::new(), values),
82        };
83        if n <= self.width as u64 {
84            samples.clear();
85            samples.resize(rows * n as usize, 0.0);
86            source.read(&self.channels, self.start..self.end, &mut samples)?;
87            *out = Envelope::Samples(samples);
88            return Ok(());
89        }
90        values.clear();
91        values.resize(rows * self.width, EMPTY);
92        if let Some(pyramid) = pyramid {
93            if pyramid.envelope(&self.channels, self.start, self.end, self.width, &mut values)? {
94                *out = Envelope::Columns { values, complete: pyramid.covers(self.start, self.end) };
95                return Ok(());
96            }
97        }
98        let columns = self.columns();
99        let ranges = chunk_aligned(self.start, self.end, source.chunk_samples(), RAW_BLOCK_VALUES / rows.max(1));
100        read_channel_blocks(source, &self.channels, &ranges, |block| fold_block(block, 0..block.samples, columns, &mut values))?;
101        finish(&mut values);
102        *out = Envelope::Columns { values, complete: true };
103        Ok(())
104    }
105}
106
107/// Blocks of `start..end` of at most `max_samples` samples (at least one storage chunk) whose
108/// edges fall on the source's storage chunks (`chunk`), so no chunk is decoded twice.
109fn chunk_aligned(start: u64, end: u64, chunk: Option<u64>, max_samples: usize) -> Vec<Range<u64>> {
110    let chunk = chunk.filter(|&c| c > 0).unwrap_or(1);
111    let step = ((max_samples as u64) / chunk).max(1) * chunk;
112    let mut ranges = Vec::new();
113    let mut b0 = start;
114    while b0 < end {
115        // The first block may start inside a chunk; every later one starts on a chunk edge
116        let b1 = (b0 / chunk * chunk + step).min(end);
117        ranges.push(b0..b1);
118        b0 = b1;
119    }
120    ranges
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn blocks_cover_the_window_on_chunk_edges() {
129        let ranges = chunk_aligned(10, 1000, Some(64), 200);
130        assert_eq!(ranges.first().map(|r| r.start), Some(10));
131        assert_eq!(ranges.last().map(|r| r.end), Some(1000));
132        assert!(ranges.windows(2).all(|w| w[0].end == w[1].start && w[1].start % 64 == 0));
133        assert!(ranges.iter().all(|r| r.end - r.start <= 192));
134        // Blocks are at least one chunk even when the budget is smaller
135        assert!(chunk_aligned(0, 1000, Some(512), 10).iter().all(|r| r.end - r.start <= 512));
136    }
137
138    #[test]
139    fn streamed_blocks_fold_like_one_read() {
140        let rec = recording();
141        let (channels, start, end, width) = (vec![0usize, 2], 37u64, 19_000u64, 300usize);
142        let columns = Columns::Even { start, len: end - start, width };
143        let mut whole = vec![EMPTY; channels.len() * width];
144        let mut data = vec![0.0; channels.len() * (end - start) as usize];
145        rec.read(&channels, start..end, &mut data).unwrap();
146        for (row, acc) in data.chunks_exact((end - start) as usize).zip(whole.chunks_exact_mut(width)) {
147            crate::envelope::fold::fold_row(row, start, columns, acc);
148        }
149        let mut streamed = vec![EMPTY; channels.len() * width];
150        let ranges = chunk_aligned(start, end, Some(1000), 2500);
151        assert!(ranges.len() > 2);
152        read_channel_blocks(&rec, &channels, &ranges, |b| fold_block(b, 0..b.samples, columns, &mut streamed)).unwrap();
153        assert_eq!(streamed, whole);
154    }
155    use dspk_core::MemoryRecording;
156
157    fn recording() -> MemoryRecording {
158        let (channels, samples) = (3usize, 20_000usize);
159        let data: Vec<f32> = (0..channels * samples).map(|i| ((i * 7919) % 1013) as f32 - 500.0).collect();
160        MemoryRecording::new("v", data, channels, 1000.0).unwrap()
161    }
162
163    #[test]
164    fn short_windows_return_samples() {
165        let rec = recording();
166        let view = View { channels: vec![2, 0], start: 100, end: 150, width: 64 };
167        let Envelope::Samples(s) = view.read(&rec, None).unwrap() else { panic!("samples expected") };
168        let mut expected = vec![0.0; 2 * 50];
169        rec.read(&[2, 0], 100..150, &mut expected).unwrap();
170        assert_eq!(s, expected);
171    }
172
173    #[test]
174    fn pyramid_and_raw_reads_agree_on_every_peak() {
175        let rec = recording();
176        let pyramid = Pyramid::in_memory(&rec, 16).unwrap();
177        let view = View { channels: vec![1], start: 0, end: 20_000, width: 50 };
178
179        let Envelope::Columns { complete, .. } = view.read(&rec, Some(&pyramid)).unwrap() else { panic!() };
180        assert!(!complete, "nothing built yet");
181
182        assert!(pyramid.fill(&rec, 0, 20_000, 1 << 20, || false).unwrap());
183        let Envelope::Columns { values: from_pyramid, complete } = view.read(&rec, Some(&pyramid)).unwrap() else { panic!() };
184        let Envelope::Columns { values: raw, .. } = view.read(&rec, None).unwrap() else { panic!() };
185        assert!(complete);
186        let span = |v: &[[f32; 2]]| v.iter().fold(EMPTY, |a, c| [a[0].min(c[0]), a[1].max(c[1])]);
187        assert_eq!(span(&from_pyramid), span(&raw), "the same extremes, column edges aside");
188    }
189
190    #[test]
191    fn invalid_views_are_rejected() {
192        let rec = recording();
193        assert!(View { channels: vec![3], start: 0, end: 10, width: 4 }.read(&rec, None).is_err());
194        assert!(View { channels: vec![0], start: 10, end: 10, width: 4 }.read(&rec, None).is_err());
195        assert!(View { channels: vec![0], start: 0, end: 20_001, width: 4 }.read(&rec, None).is_err());
196    }
197}