extern crate alloc;
#[cfg(feature = "read")]
pub mod inspect;
#[cfg(feature = "read")]
use std::{
fs::File,
io::{BufReader, Seek, SeekFrom},
path::{Path, PathBuf},
sync::OnceLock,
vec::Vec,
};
#[cfg(feature = "read")]
use crate::{
domain::{Channel, ChannelData, ChannelMetadata, Datafile, GraphMetadata, Journal, Marker},
error::{BiopacError, ParseResult, Warning},
parser::{headers::parse_headers, markers::parse_markers_and_journal, reader::read_stream},
};
#[derive(Debug, Clone, Default)]
pub struct ReadOptions {
channel_indices: Option<alloc::vec::Vec<usize>>,
scaled: bool,
}
impl ReadOptions {
pub const fn new() -> Self {
Self {
channel_indices: None,
scaled: false,
}
}
#[must_use]
pub fn channels(mut self, indices: &[usize]) -> Self {
self.channel_indices = Some(indices.to_vec());
self
}
#[must_use]
pub const fn scaled(mut self, scaled: bool) -> Self {
self.scaled = scaled;
self
}
#[must_use]
pub const fn build(self) -> Self {
self
}
#[cfg(feature = "read")]
pub fn read_file(self, path: impl AsRef<Path>) -> Result<ParseResult<Datafile>, BiopacError> {
let result = crate::parser::reader::read_file(path)?;
Ok(self.apply(result))
}
#[cfg(feature = "read")]
pub fn read_bytes(self, bytes: &[u8]) -> Result<ParseResult<Datafile>, BiopacError> {
let result = read_stream(std::io::Cursor::new(bytes))?;
Ok(self.apply(result))
}
#[cfg(feature = "read")]
pub fn read_stream<R: std::io::Read + std::io::Seek>(
self,
reader: R,
) -> Result<ParseResult<Datafile>, BiopacError> {
let result = read_stream(reader)?;
Ok(self.apply(result))
}
fn apply(self, mut result: ParseResult<Datafile>) -> ParseResult<Datafile> {
if let Some(ref keep) = self.channel_indices {
let all = core::mem::take(&mut result.value.channels);
result.value.channels = keep.iter().filter_map(|&i| all.get(i).cloned()).collect();
}
if self.scaled {
for ch in &mut result.value.channels {
if matches!(&ch.data, ChannelData::Scaled { .. }) {
let floats = ch.scaled_samples();
ch.data = ChannelData::Float(floats);
}
}
}
result
}
}
#[cfg(feature = "read")]
pub struct LazyDatafile {
pub metadata: GraphMetadata,
pub channel_metadata: Vec<ChannelMetadata>,
pub markers: Vec<Marker>,
pub journal: Option<Journal>,
pub warnings: Vec<Warning>,
path: PathBuf,
data_loaded: OnceLock<Vec<Channel>>,
}
#[cfg(feature = "read")]
impl LazyDatafile {
pub const fn channel_count(&self) -> usize {
self.channel_metadata.len()
}
pub fn is_data_loaded(&self) -> bool {
self.data_loaded.get().is_some()
}
pub fn load_channel(&self, index: usize) -> Result<&Channel, BiopacError> {
let channels = self.ensure_loaded()?;
channels.get(index).ok_or_else(|| {
BiopacError::InvalidChannel(alloc::format!(
"index {index} out of bounds (file has {} channels)",
channels.len()
))
})
}
pub fn load_all(&self) -> Result<&[Channel], BiopacError> {
self.ensure_loaded()
}
pub fn into_datafile(self) -> Result<Datafile, BiopacError> {
let channels = if let Some(ch) = self.data_loaded.into_inner() {
ch
} else {
let file = File::open(&self.path)?;
let reader = BufReader::new(file);
read_stream(reader)?.value.channels
};
Ok(Datafile {
metadata: self.metadata,
channels,
markers: self.markers,
journal: self.journal,
})
}
fn ensure_loaded(&self) -> Result<&[Channel], BiopacError> {
if let Some(ch) = self.data_loaded.get() {
return Ok(ch);
}
let file = File::open(&self.path)?;
let reader = BufReader::new(file);
let channels = read_stream(reader)?.value.channels;
let _ = self.data_loaded.set(channels);
self.data_loaded.get().map(Vec::as_slice).ok_or_else(|| {
BiopacError::Validation(alloc::string::String::from("OnceLock invariant violated"))
})
}
}
#[cfg(feature = "read")]
impl core::fmt::Debug for LazyDatafile {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("LazyDatafile")
.field("metadata", &self.metadata)
.field("channel_count", &self.channel_metadata.len())
.field("marker_count", &self.markers.len())
.field("journal", &self.journal)
.field("warnings", &self.warnings)
.field("path", &self.path)
.field("data_loaded", &self.data_loaded.get().is_some())
.finish()
}
}
#[cfg(feature = "read")]
pub fn open_file(path: impl AsRef<Path>) -> Result<LazyDatafile, BiopacError> {
let path_buf = path.as_ref().to_path_buf();
let file = File::open(&path_buf)?;
let mut reader = BufReader::new(file);
let headers = parse_headers(&mut reader)?;
let display_orders: Vec<u16> = headers
.channel_metadata
.iter()
.map(|m| m.display_order)
.collect();
let file_revision = headers.graph_metadata.file_revision.0;
let compressed = headers.graph_metadata.compressed;
let (markers, journal, extra_warnings) = if compressed {
let (m, j, mw) = parse_markers_lazy(&mut reader, file_revision, &display_orders);
(m, j, mw)
} else {
if let Some(size) = headers.uncompressed_data_byte_count() {
let target = headers.data_start_offset + size;
match reader.seek(SeekFrom::Start(target)) {
Ok(_) => {
let (m, j, mw) =
parse_markers_lazy(&mut reader, file_revision, &display_orders);
(m, j, mw)
}
Err(e) => {
let w = Warning::new(alloc::format!(
"LazyDatafile: could not seek past data section: {e}"
));
(Vec::new(), None, alloc::vec![w])
}
}
} else {
let w = Warning::new(alloc::string::String::from(
"LazyDatafile: sample counts not set in headers; \
markers not parsed (use into_datafile() to load all data)",
));
(Vec::new(), None, alloc::vec![w])
}
};
let metadata = headers.graph_metadata;
let channel_metadata = headers.channel_metadata;
let mut warnings = headers.warnings;
warnings.extend(extra_warnings);
Ok(LazyDatafile {
metadata,
channel_metadata,
markers,
journal,
warnings,
path: path_buf,
data_loaded: OnceLock::new(),
})
}
#[cfg(feature = "read")]
fn parse_markers_lazy<R: std::io::Read + std::io::Seek>(
reader: &mut R,
file_revision: i32,
display_orders: &[u16],
) -> (Vec<Marker>, Option<Journal>, Vec<Warning>) {
match parse_markers_and_journal(reader, file_revision, display_orders) {
Ok(mj) => (mj.markers, mj.journal, mj.warnings),
Err(e) => {
let w = Warning::new(alloc::format!("marker section unreadable: {e}"));
(Vec::new(), None, alloc::vec![w])
}
}
}