use std::ops::Range;
use dspk_core::{DspError, DspResult, RecordingSource};
use crate::envelope::fold::{finish, fold_block, Columns, EMPTY};
use crate::pyramid::Pyramid;
use crate::read::read_channel_blocks;
pub const RAW_BLOCK_VALUES: usize = 1 << 22;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct View {
pub channels: Vec<usize>,
pub start: u64,
pub end: u64,
pub width: usize,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Envelope {
Samples(Vec<f32>),
Columns { values: Vec<[f32; 2]>, complete: bool },
}
impl View {
pub fn samples(&self) -> u64 {
self.end.saturating_sub(self.start)
}
pub fn columns(&self) -> Columns {
Columns::Even { start: self.start, len: self.samples(), width: self.width }
}
pub fn validate(&self, channels: usize, samples: u64) -> DspResult<()> {
if let Some(&channel) = self.channels.iter().find(|&&c| c >= channels) {
return Err(DspError::InvalidChannel { channel, total: channels });
}
if self.start >= self.end || self.end > samples || self.width == 0 {
return Err(DspError::SampleRange { start: self.start, end: self.end, total: samples });
}
Ok(())
}
pub fn read(&self, source: &dyn RecordingSource, pyramid: Option<&Pyramid>) -> DspResult<Envelope> {
let mut out = Envelope::Samples(Vec::new());
self.read_into(source, pyramid, &mut out)?;
Ok(out)
}
pub fn read_into(&self, source: &dyn RecordingSource, pyramid: Option<&Pyramid>, out: &mut Envelope) -> DspResult<()> {
let info = source.info();
self.validate(info.channel_count(), info.samples)?;
let (rows, n) = (self.channels.len(), self.samples());
let (mut samples, mut values) = match std::mem::replace(out, Envelope::Samples(Vec::new())) {
Envelope::Samples(s) => (s, Vec::new()),
Envelope::Columns { values, .. } => (Vec::new(), values),
};
if n <= self.width as u64 {
samples.clear();
samples.resize(rows * n as usize, 0.0);
source.read(&self.channels, self.start..self.end, &mut samples)?;
*out = Envelope::Samples(samples);
return Ok(());
}
values.clear();
values.resize(rows * self.width, EMPTY);
if let Some(pyramid) = pyramid {
if pyramid.envelope(&self.channels, self.start, self.end, self.width, &mut values)? {
*out = Envelope::Columns { values, complete: pyramid.covers(self.start, self.end) };
return Ok(());
}
}
let columns = self.columns();
let ranges = chunk_aligned(self.start, self.end, source.chunk_samples(), RAW_BLOCK_VALUES / rows.max(1));
read_channel_blocks(source, &self.channels, &ranges, |block| fold_block(block, 0..block.samples, columns, &mut values))?;
finish(&mut values);
*out = Envelope::Columns { values, complete: true };
Ok(())
}
}
fn chunk_aligned(start: u64, end: u64, chunk: Option<u64>, max_samples: usize) -> Vec<Range<u64>> {
let chunk = chunk.filter(|&c| c > 0).unwrap_or(1);
let step = ((max_samples as u64) / chunk).max(1) * chunk;
let mut ranges = Vec::new();
let mut b0 = start;
while b0 < end {
let b1 = (b0 / chunk * chunk + step).min(end);
ranges.push(b0..b1);
b0 = b1;
}
ranges
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_cover_the_window_on_chunk_edges() {
let ranges = chunk_aligned(10, 1000, Some(64), 200);
assert_eq!(ranges.first().map(|r| r.start), Some(10));
assert_eq!(ranges.last().map(|r| r.end), Some(1000));
assert!(ranges.windows(2).all(|w| w[0].end == w[1].start && w[1].start % 64 == 0));
assert!(ranges.iter().all(|r| r.end - r.start <= 192));
assert!(chunk_aligned(0, 1000, Some(512), 10).iter().all(|r| r.end - r.start <= 512));
}
#[test]
fn streamed_blocks_fold_like_one_read() {
let rec = recording();
let (channels, start, end, width) = (vec![0usize, 2], 37u64, 19_000u64, 300usize);
let columns = Columns::Even { start, len: end - start, width };
let mut whole = vec![EMPTY; channels.len() * width];
let mut data = vec![0.0; channels.len() * (end - start) as usize];
rec.read(&channels, start..end, &mut data).unwrap();
for (row, acc) in data.chunks_exact((end - start) as usize).zip(whole.chunks_exact_mut(width)) {
crate::envelope::fold::fold_row(row, start, columns, acc);
}
let mut streamed = vec![EMPTY; channels.len() * width];
let ranges = chunk_aligned(start, end, Some(1000), 2500);
assert!(ranges.len() > 2);
read_channel_blocks(&rec, &channels, &ranges, |b| fold_block(b, 0..b.samples, columns, &mut streamed)).unwrap();
assert_eq!(streamed, whole);
}
use dspk_core::MemoryRecording;
fn recording() -> MemoryRecording {
let (channels, samples) = (3usize, 20_000usize);
let data: Vec<f32> = (0..channels * samples).map(|i| ((i * 7919) % 1013) as f32 - 500.0).collect();
MemoryRecording::new("v", data, channels, 1000.0).unwrap()
}
#[test]
fn short_windows_return_samples() {
let rec = recording();
let view = View { channels: vec![2, 0], start: 100, end: 150, width: 64 };
let Envelope::Samples(s) = view.read(&rec, None).unwrap() else { panic!("samples expected") };
let mut expected = vec![0.0; 2 * 50];
rec.read(&[2, 0], 100..150, &mut expected).unwrap();
assert_eq!(s, expected);
}
#[test]
fn pyramid_and_raw_reads_agree_on_every_peak() {
let rec = recording();
let pyramid = Pyramid::in_memory(&rec, 16).unwrap();
let view = View { channels: vec![1], start: 0, end: 20_000, width: 50 };
let Envelope::Columns { complete, .. } = view.read(&rec, Some(&pyramid)).unwrap() else { panic!() };
assert!(!complete, "nothing built yet");
assert!(pyramid.fill(&rec, 0, 20_000, 1 << 20, || false).unwrap());
let Envelope::Columns { values: from_pyramid, complete } = view.read(&rec, Some(&pyramid)).unwrap() else { panic!() };
let Envelope::Columns { values: raw, .. } = view.read(&rec, None).unwrap() else { panic!() };
assert!(complete);
let span = |v: &[[f32; 2]]| v.iter().fold(EMPTY, |a, c| [a[0].min(c[0]), a[1].max(c[1])]);
assert_eq!(span(&from_pyramid), span(&raw), "the same extremes, column edges aside");
}
#[test]
fn invalid_views_are_rejected() {
let rec = recording();
assert!(View { channels: vec![3], start: 0, end: 10, width: 4 }.read(&rec, None).is_err());
assert!(View { channels: vec![0], start: 10, end: 10, width: 4 }.read(&rec, None).is_err());
assert!(View { channels: vec![0], start: 0, end: 20_001, width: 4 }.read(&rec, None).is_err());
}
}