Skip to main content

dspk_io/core/
sources.rs

1//! The signals ("sources") a file offers, listed cheaply and opened on demand.
2//!
3//! A multi-signal file (e.g. an NWB store with several series, a SpikeGLX run with several
4//! streams) lists one entry per signal; plain recordings are one source, [`MAIN`]. Listing reads
5//! only metadata.
6
7use std::path::Path;
8
9use dspk_core::{DspError, DspResult, RationalTime, RecordingSource, SampleFormat, SampleRate, SignalUnit};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum SourceKind {
13    /// Electric potential (values in a volt unit).
14    Electrical,
15    /// Anything else (pressure, temperature, analog inputs, …).
16    Other,
17}
18
19impl SourceKind {
20    /// [`SourceKind::Electrical`] for volt units, else [`SourceKind::Other`].
21    pub fn from_unit(unit: &SignalUnit) -> Self {
22        match unit {
23            SignalUnit::Volt | SignalUnit::Millivolt | SignalUnit::Microvolt => SourceKind::Electrical,
24            _ => SourceKind::Other,
25        }
26    }
27}
28
29/// One openable signal of a file.
30#[derive(Debug, Clone, PartialEq)]
31pub struct SourceEntry {
32    /// Stable id within the file (e.g. NWB series path, SpikeGLX file name, [`MAIN`]).
33    pub id: String,
34    pub name: String,
35    pub kind: SourceKind,
36    pub channels: usize,
37    pub samples: u64,
38    pub sample_rate: SampleRate,
39    pub start_time: RationalTime,
40    /// Stored sample type.
41    pub format: SampleFormat,
42    /// Unit of the values a read returns.
43    pub unit: SignalUnit,
44}
45
46impl SourceEntry {
47    pub fn duration_sec(&self) -> f64 {
48        self.samples as f64 / self.sample_rate.rate_hz()
49    }
50
51    /// e.g. `HDEMG · 32 ch · 24.4 kHz · µV`
52    pub fn summary(&self) -> String {
53        let hz = self.sample_rate.rate_hz();
54        let rate = if hz >= 1000.0 { format!("{:.1} kHz", hz / 1000.0) } else { format!("{hz:.1} Hz") };
55        format!("{} · {} ch · {rate} · {}", self.name, self.channels, self.unit.symbol())
56    }
57}
58
59/// The id of the only source of a single-recording file.
60pub const MAIN: &str = "main";
61
62/// The [`MAIN`] entry describing an opened single-recording file.
63pub fn single_source(rec: &dyn RecordingSource) -> SourceEntry {
64    let info = rec.info();
65    let unit = info.channels.first().map(|c| c.unit.clone()).unwrap_or_default();
66    SourceEntry {
67        id: MAIN.into(),
68        name: info.name.clone(),
69        kind: SourceKind::from_unit(&unit),
70        channels: info.channel_count(),
71        samples: info.samples,
72        sample_rate: info.sample_rate,
73        start_time: info.start_time,
74        format: info.format,
75        unit,
76    }
77}
78
79/// `Ok` when `id` is [`MAIN`], for single-recording formats.
80pub fn require_main(path: &Path, id: &str) -> DspResult<()> {
81    if id == MAIN {
82        Ok(())
83    } else {
84        Err(DspError::InvalidConfig(format!("{} has no source {id}", path.display())))
85    }
86}
87
88/// The source to show first: the largest electrical source, else the largest one.
89pub fn default_source(list: &[SourceEntry]) -> Option<&SourceEntry> {
90    // Most data first; equal sizes: the higher sample rate (e.g. SpikeGLX ap over lf)
91    let size = |s: &&SourceEntry| (s.samples * s.channels as u64, s.sample_rate.as_ratio());
92    list.iter().filter(|s| s.kind == SourceKind::Electrical).max_by_key(size).or_else(|| list.iter().max_by_key(size))
93}