use std::ops::Range;
use std::path::Path;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use dspk_core::{DspResult, RecordingInfo, RecordingSource};
use crate::pyramid::{pyramid_path, OnProgress, Progress, Pyramid, PyramidBuilder, PyramidIdentity, FILE_BASE, MEMORY_BASE};
use crate::view::{Envelope, View, RAW_BLOCK_VALUES};
pub const FILE_PYRAMID_MIN_BYTES: u64 = 64 << 20;
pub trait SignalBackend: Send + Sync {
fn info(&self) -> &RecordingInfo;
fn view(&self, view: &View, out: &mut Envelope) -> DspResult<()>;
fn read(&self, channels: &[usize], samples: Range<u64>, out: &mut [f32]) -> DspResult<()>;
fn focus(&self, sample: u64);
fn prepare(&self, start: u64, end: u64) -> DspResult<()>;
fn progress(&self) -> Progress;
fn watch(&self, on_progress: OnProgress);
}
pub struct LocalSignal {
source: Arc<dyn RecordingSource>,
pyramid: Arc<Pyramid>,
builder: PyramidBuilder,
cancel: Arc<AtomicBool>,
done: Arc<AtomicU64>,
listeners: Arc<Mutex<Vec<OnProgress>>>,
}
impl LocalSignal {
pub fn open(source: Arc<dyn RecordingSource>, recording: Option<(&Path, &str)>) -> DspResult<Self> {
let pyramid = Self::choose_pyramid(source.as_ref(), recording)?;
let total = source.info().samples;
let done = if pyramid.is_complete() { total } else { 0 };
Ok(Self {
source,
pyramid: Arc::new(pyramid),
builder: PyramidBuilder::default(),
cancel: Arc::new(AtomicBool::new(false)),
done: Arc::new(AtomicU64::new(done)),
listeners: Arc::new(Mutex::new(Vec::new())),
})
}
fn choose_pyramid(source: &dyn RecordingSource, recording: Option<(&Path, &str)>) -> DspResult<Pyramid> {
let Some((path, id)) = recording else { return Pyramid::in_memory(source, MEMORY_BASE) };
let identity = PyramidIdentity::of(path, id, source)?;
let file = pyramid_path(path, id);
if let Some(complete) = Pyramid::open_file(&file, &identity, FILE_BASE)? {
return Ok(complete);
}
if source.info().data_bytes() < FILE_PYRAMID_MIN_BYTES {
return Pyramid::in_memory(source, MEMORY_BASE);
}
match Pyramid::create_file(&file, &identity, FILE_BASE) {
Ok(pyramid) => Ok(pyramid),
Err(e) => {
tracing::warn!("cannot write {}: {e}; keeping the min/max pyramid in memory", file.display());
Pyramid::in_memory(source, MEMORY_BASE)
}
}
}
pub fn source(&self) -> &Arc<dyn RecordingSource> {
&self.source
}
pub fn pyramid(&self) -> &Arc<Pyramid> {
&self.pyramid
}
}
impl SignalBackend for LocalSignal {
fn info(&self) -> &RecordingInfo {
self.source.info()
}
fn view(&self, view: &View, out: &mut Envelope) -> DspResult<()> {
view.read_into(self.source.as_ref(), Some(&self.pyramid), out)
}
fn read(&self, channels: &[usize], samples: Range<u64>, out: &mut [f32]) -> DspResult<()> {
self.source.read(channels, samples, out)
}
fn focus(&self, sample: u64) {
if self.pyramid.is_complete() {
return;
}
let (done, listeners) = (self.done.clone(), self.listeners.clone());
let on_progress: OnProgress = Arc::new(move |p: Progress| {
done.store(p.done, Ordering::Relaxed);
for listener in listeners.lock().expect("listeners lock").iter() {
listener(p.clone());
}
});
self.builder.run(self.source.clone(), self.pyramid.clone(), sample, self.cancel.clone(), on_progress);
}
fn prepare(&self, start: u64, end: u64) -> DspResult<()> {
let end = end.min(self.source.info().samples);
if start < end {
self.pyramid.fill(self.source.as_ref(), start, end, RAW_BLOCK_VALUES, || false)?;
}
Ok(())
}
fn progress(&self) -> Progress {
Progress { filled: 0..0, done: self.done.load(Ordering::Relaxed), total: self.source.info().samples }
}
fn watch(&self, on_progress: OnProgress) {
self.listeners.lock().expect("listeners lock").push(on_progress);
}
}
impl Drop for LocalSignal {
fn drop(&mut self) {
self.cancel.store(true, Ordering::Relaxed);
}
}
#[cfg(test)]
mod tests {
use super::*;
use dspk_core::MemoryRecording;
#[test]
fn small_recordings_keep_the_pyramid_in_memory_and_fill_it() {
let data: Vec<f32> = (0..2 * 50_000).map(|i| ((i * 7919) % 101) as f32).collect();
let source: Arc<dyn RecordingSource> = Arc::new(MemoryRecording::new("s", data, 2, 1000.0).unwrap());
let signal = LocalSignal::open(source, None).unwrap();
assert!(signal.pyramid().path().is_none());
assert_eq!(signal.progress().done, 0);
let (tx, rx) = std::sync::mpsc::channel();
signal.watch(Arc::new(move |p: Progress| {
if p.done == p.total {
let _ = tx.send(());
}
}));
signal.focus(25_000);
rx.recv_timeout(std::time::Duration::from_secs(30)).expect("pyramid filled");
assert!(signal.pyramid().is_complete());
let mut out = Envelope::Samples(Vec::new());
signal.view(&View { channels: vec![1], start: 0, end: 50_000, width: 10 }, &mut out).unwrap();
assert!(matches!(out, Envelope::Columns { complete: true, .. }));
}
}