//! Recording metadata and information. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; /// Information about a recording session #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecordingInfo { /// Subject ID pub subject_id: String, /// Recording date and time pub recording_date: Option>, /// Recording device/system name pub device: String, /// Device serial number pub device_serial: Option, /// Experimenter name pub experimenter: Option, /// Recording location/institution pub institution: Option, /// Description/comments pub description: String, /// Line frequency (50 or 60 Hz) pub line_freq: Option, /// High-pass filter setting during acquisition pub highpass: Option, /// Low-pass filter setting during acquisition pub lowpass: Option, } impl RecordingInfo { /// Create a new recording info with minimal required fields #[must_use] pub fn new(subject_id: impl Into) -> Self { Self { subject_id: subject_id.into(), recording_date: None, device: "Unknown".to_string(), device_serial: None, experimenter: None, institution: None, description: String::new(), line_freq: None, highpass: None, lowpass: None, } } /// Set recording date #[must_use] pub fn with_date(mut self, date: DateTime) -> Self { self.recording_date = Some(date); self } /// Set device information #[must_use] pub fn with_device(mut self, device: impl Into) -> Self { self.device = device.into(); self } /// Set line frequency #[must_use] pub fn with_line_freq(mut self, freq: f64) -> Self { self.line_freq = Some(freq); self } } impl Default for RecordingInfo { fn default() -> Self { Self::new("unknown") } } /// Handle to a loaded recording #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Recording { /// Unique identifier pub id: uuid::Uuid, /// File path pub path: std::path::PathBuf, /// Recording metadata pub info: RecordingInfo, /// Format type pub format: RecordingFormat, /// Whether data is currently loaded in memory pub loaded: bool, } impl Recording { /// Create a new recording handle #[must_use] pub fn new( path: impl Into, info: RecordingInfo, format: RecordingFormat, ) -> Self { Self { id: uuid::Uuid::new_v4(), path: path.into(), info, format, loaded: false, } } } /// Supported recording file formats #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum RecordingFormat { /// European Data Format Edf, /// BioSemi Data Format (24-bit EDF variant) Bdf, /// BrainVision format (.vhdr/.vmrk/.eeg) BrainVision, /// EEGLAB format (.set) EegLab, /// Elekta/Neuromag FIF format Fif, /// CTF MEG format (.ds folder) Ctf, /// 4D Neuroimaging format FourD, /// Yokogawa/Ricoh MEG format Yokogawa, /// KIT MEG format Kit, /// Curry format Curry, /// Neuroscan format (.cnt, .eeg) Neuroscan, /// Nihon Kohden format NihonKohden, /// BIDS dataset Bids, /// Neurodata Without Borders Nwb, /// Generic binary format Binary, /// Unknown format Unknown, } impl RecordingFormat { /// Detect format from file extension #[must_use] pub fn from_extension(ext: &str) -> Self { match ext.to_lowercase().as_str() { "edf" => Self::Edf, "bdf" => Self::Bdf, "vhdr" | "vmrk" | "eeg" => Self::BrainVision, "set" => Self::EegLab, "fif" => Self::Fif, "ds" => Self::Ctf, "cnt" => Self::Neuroscan, "nwb" => Self::Nwb, _ => Self::Unknown, } } /// Get typical file extensions for this format #[must_use] pub fn extensions(&self) -> &[&str] { match self { Self::Edf => &["edf"], Self::Bdf => &["bdf"], Self::BrainVision => &["vhdr", "vmrk", "eeg"], Self::EegLab => &["set"], Self::Fif => &["fif", "fif.gz"], Self::Ctf => &["ds"], Self::Neuroscan => &["cnt", "eeg", "avg"], Self::NihonKohden => &["eeg"], Self::Nwb => &["nwb"], Self::Yokogawa | Self::Kit => &["sqd", "con", "raw", "ave"], _ => &[], } } /// Whether this format supports continuous raw data #[must_use] pub fn supports_raw(&self) -> bool { matches!( self, Self::Edf | Self::Bdf | Self::BrainVision | Self::Fif | Self::Ctf | Self::FourD | Self::Neuroscan ) } /// Whether this format supports epochs #[must_use] pub fn supports_epochs(&self) -> bool { matches!(self, Self::EegLab | Self::Fif | Self::Nwb) } /// Whether this is an MEG format #[must_use] pub fn is_meg(&self) -> bool { matches!( self, Self::Fif | Self::Ctf | Self::FourD | Self::Yokogawa | Self::Kit ) } /// Whether this is an EEG format #[must_use] pub fn is_eeg(&self) -> bool { matches!( self, Self::Edf | Self::Bdf | Self::BrainVision | Self::EegLab | Self::Neuroscan | Self::NihonKohden | Self::Curry ) } } /// Processing history for a recording #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessingHistory { /// List of processing steps applied steps: Vec, } impl ProcessingHistory { /// Create a new empty history #[must_use] pub fn new() -> Self { Self { steps: Vec::new() } } /// Add a processing step pub fn add(&mut self, step: ProcessingStep) { self.steps.push(step); } /// Get all steps #[must_use] pub fn steps(&self) -> &[ProcessingStep] { &self.steps } } impl Default for ProcessingHistory { fn default() -> Self { Self::new() } } /// A single processing step #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessingStep { /// Name of the processing function pub function: String, /// Parameters used pub parameters: serde_json::Value, /// Timestamp when applied pub timestamp: DateTime, /// Description pub description: Option, } impl ProcessingStep { /// Create a new processing step #[must_use] pub fn new(function: impl Into, parameters: serde_json::Value) -> Self { Self { function: function.into(), parameters, timestamp: Utc::now(), description: None, } } /// Add description #[must_use] pub fn with_description(mut self, desc: impl Into) -> Self { self.description = Some(desc.into()); self } } #[cfg(test)] mod tests { use super::*; #[test] fn test_format_detection() { assert_eq!(RecordingFormat::from_extension("edf"), RecordingFormat::Edf); assert_eq!(RecordingFormat::from_extension("BDF"), RecordingFormat::Bdf); assert_eq!( RecordingFormat::from_extension("vhdr"), RecordingFormat::BrainVision ); } #[test] fn test_format_capabilities() { assert!(RecordingFormat::Edf.is_eeg()); assert!(!RecordingFormat::Edf.is_meg()); assert!(RecordingFormat::Fif.is_meg()); assert!(RecordingFormat::Edf.supports_raw()); } }