Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -0,0 +1,132 @@
//! EGI Format Constants
//!
//! File offsets, magic numbers, and type definitions for EGI files.
/// Magic header for EGI simple binary format
pub const EGI_RAW_MAGIC: &[u8; 4] = b"VERS";
/// Header line start for version
pub const EGI_VERSION_TAG: &str = "Version";
/// Header line start for sample rate
pub const EGI_SFREQ_TAG: &str = "Sample Rate";
/// Header line start for channel count
pub const EGI_NCHAN_TAG: &str = "Number of Channels";
/// Header line start for gain
pub const EGI_GAIN_TAG: &str = "Gain";
/// Header line start for number of samples
pub const EGI_NSAMP_TAG: &str = "Number of Samples";
/// Header line start for precision
pub const EGI_PRECISION_TAG: &str = "Precision";
/// Header line start for number of categories
pub const EGI_NCATS_TAG: &str = "Number of Categories";
/// Header line start for category name
pub const EGI_CATEGORY_TAG: &str = "Category";
// Data types
/// Float32 precision
pub const EGI_DTYPE_FLOAT: i16 = 4;
/// Int16 precision
pub const EGI_DTYPE_INT16: i16 = 2;
/// Float64 precision
pub const EGI_DTYPE_DOUBLE: i16 = 8;
// EGI sensor net sizes
/// 32-channel net
pub const EGI_NET_32: usize = 32;
/// 64-channel net
pub const EGI_NET_64: usize = 64;
/// 128-channel net
pub const EGI_NET_128: usize = 128;
/// 256-channel net
pub const EGI_NET_256: usize = 256;
// MFF file names
/// MFF info file
pub const MFF_INFO_FILE: &str = "info.xml";
/// MFF signal file pattern
pub const MFF_SIGNAL_PREFIX: &str = "signal";
/// MFF coordinates file
pub const MFF_COORDS_FILE: &str = "coordinates.xml";
/// MFF categories file
pub const MFF_CATEGORIES_FILE: &str = "categories.xml";
/// MFF events file
pub const MFF_EVENTS_FILE: &str = "Events.xml";
/// Data type enumeration for EGI files
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EgiDataType {
/// 16-bit signed integer
Int16,
/// 32-bit float
Float32,
/// 64-bit float
Float64,
}
impl TryFrom<i16> for EgiDataType {
type Error = &'static str;
fn try_from(value: i16) -> Result<Self, Self::Error> {
match value {
EGI_DTYPE_INT16 => Ok(Self::Int16),
EGI_DTYPE_FLOAT => Ok(Self::Float32),
EGI_DTYPE_DOUBLE => Ok(Self::Float64),
_ => Err("Unknown EGI data type"),
}
}
}
impl EgiDataType {
/// Size of this data type in bytes
pub fn size(&self) -> usize {
match self {
Self::Int16 => 2,
Self::Float32 => 4,
Self::Float64 => 8,
}
}
/// Create from byte count
pub fn from_bytes(bytes: usize) -> Self {
match bytes {
2 => Self::Int16,
8 => Self::Float64,
_ => Self::Float32,
}
}
}
/// Channel type for EGI
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EgiChannelType {
/// Standard EEG electrode
Eeg,
/// Reference electrode (e.g., Cz)
Ref,
/// Trigger/event channel
Event,
/// PNS (photoplethysmograph, etc.)
Pns,
/// Other auxiliary channel
Other,
}
impl EgiChannelType {
/// Get string representation
pub fn as_str(&self) -> &'static str {
match self {
Self::Eeg => "EEG",
Self::Ref => "REF",
Self::Event => "EVENT",
Self::Pns => "PNS",
Self::Other => "OTHER",
}
}
}
@@ -0,0 +1,438 @@
//! EGI Header Parser
//!
//! Parses both simple RAW format and MFF format headers.
use crate::{IoError, IoResult};
use std::fs::{self, File};
use std::io::{BufRead, BufReader, Read};
use std::path::Path;
use super::constants::*;
/// EGI file format type
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EgiFormat {
/// Simple binary RAW format
Raw,
/// MFF directory format
Mff,
}
/// EGI channel kind
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum EgiChannelKind {
/// EEG electrode
Eeg,
/// Reference electrode
Ref,
/// Event/trigger channel
Event,
/// Peripheral channel (PNS, etc.)
Pns,
/// Unknown channel type
Unknown,
}
impl EgiChannelKind {
/// Get string representation
pub fn as_str(&self) -> &'static str {
match self {
Self::Eeg => "EEG",
Self::Ref => "REF",
Self::Event => "EVENT",
Self::Pns => "PNS",
Self::Unknown => "UNKNOWN",
}
}
}
/// EGI channel information
#[derive(Debug, Clone)]
pub struct EgiChannel {
/// Channel name (e.g., "E1", "E2", or "Cz")
pub name: String,
/// Channel index (0-based)
pub index: usize,
/// Channel type
pub kind: EgiChannelKind,
/// Calibration factor
pub cal: f64,
}
impl EgiChannel {
/// Get unit string
pub fn units(&self) -> &'static str {
match self.kind {
EgiChannelKind::Eeg | EgiChannelKind::Ref => "uV",
EgiChannelKind::Event => "V",
_ => "AU",
}
}
}
/// Parsed EGI header
#[derive(Debug, Clone)]
pub struct EgiHeader {
/// File format type
pub format: EgiFormat,
/// Format version
pub version: u32,
/// Sampling frequency in Hz
pub sfreq: f64,
/// Number of channels
pub n_channels: usize,
/// Number of samples
pub n_samples: usize,
/// Data type
pub data_type: EgiDataType,
/// Gain/calibration
pub gain: f64,
/// Number of event categories
pub n_categories: usize,
/// Category names
pub categories: Vec<String>,
/// Channel definitions
pub channels: Vec<EgiChannel>,
/// Header size in bytes (offset to data)
pub header_size: usize,
}
impl EgiHeader {
/// Parse a simple RAW format header
pub fn from_raw_file(path: impl AsRef<Path>) -> IoResult<Self> {
let path = path.as_ref();
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut version = 0u32;
let mut sfreq = 0.0f64;
let mut n_channels = 0usize;
let mut n_samples = 0usize;
let mut gain = 1.0f64;
let mut precision = 4i16;
let mut n_categories = 0usize;
let mut categories = Vec::new();
let mut header_lines = 0usize;
// Read ASCII header lines
loop {
let mut line = String::new();
let bytes_read = reader.read_line(&mut line)?;
if bytes_read == 0 {
break;
}
header_lines += 1;
let line = line.trim();
// Empty line marks end of header in some versions
if line.is_empty() && header_lines > 5 {
break;
}
// Check for binary data start (usually starts with non-ASCII)
if line
.as_bytes()
.first()
.map(|&b| b < 32 || b > 126)
.unwrap_or(false)
{
break;
}
// Parse key-value pairs
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
match key {
k if k.starts_with(EGI_VERSION_TAG) => {
version = value.parse().unwrap_or(0);
}
k if k.starts_with(EGI_SFREQ_TAG) => {
sfreq = value.parse().unwrap_or(0.0);
}
k if k.starts_with(EGI_NCHAN_TAG) => {
n_channels = value.parse().unwrap_or(0);
}
k if k.starts_with(EGI_NSAMP_TAG) => {
n_samples = value.parse().unwrap_or(0);
}
k if k.starts_with(EGI_GAIN_TAG) => {
gain = value.parse().unwrap_or(1.0);
}
k if k.starts_with(EGI_PRECISION_TAG) => {
precision = value.parse().unwrap_or(4);
}
k if k.starts_with(EGI_NCATS_TAG) => {
n_categories = value.parse().unwrap_or(0);
}
k if k.starts_with(EGI_CATEGORY_TAG) => {
categories.push(value.to_string());
}
_ => {}
}
}
// Safety limit on header lines
if header_lines > 1000 {
return Err(IoError::InvalidFormat("EGI header too long".to_string()));
}
}
// Determine data type from precision
let data_type = EgiDataType::from_bytes(precision as usize);
// Calculate header size (approximate)
let header_size = Self::find_data_offset(path, n_channels, data_type)?;
// Generate channel names
let channels = Self::generate_channels(n_channels, gain);
Ok(Self {
format: EgiFormat::Raw,
version,
sfreq,
n_channels,
n_samples,
data_type,
gain,
n_categories,
categories,
channels,
header_size,
})
}
/// Parse an MFF directory
pub fn from_mff_dir(path: impl AsRef<Path>) -> IoResult<Self> {
let path = path.as_ref();
if !path.is_dir() {
return Err(IoError::InvalidFormat(
"MFF path is not a directory".to_string(),
));
}
// Read info.xml
let info_path = path.join(MFF_INFO_FILE);
let (sfreq, n_channels) = if info_path.exists() {
Self::parse_info_xml(&info_path)?
} else {
(256.0, 0)
};
// Find signal files and determine n_samples
let mut signal_files = Vec::new();
for entry in fs::read_dir(path)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(MFF_SIGNAL_PREFIX) && name.ends_with(".bin") {
signal_files.push(entry.path());
}
}
signal_files.sort();
// Determine n_samples and n_channels from first signal file
let (n_samples, actual_n_channels, data_type) = if let Some(sig_path) = signal_files.first()
{
Self::parse_signal_file(sig_path)?
} else {
return Err(IoError::FileNotFound(
"No signal files found in MFF directory".to_string(),
));
};
let n_channels = if n_channels > 0 {
n_channels
} else {
actual_n_channels
};
// Generate channel names
let channels = Self::generate_channels(n_channels, 1.0);
Ok(Self {
format: EgiFormat::Mff,
version: 0,
sfreq,
n_channels,
n_samples,
data_type,
gain: 1.0,
n_categories: 0,
categories: Vec::new(),
channels,
header_size: 0, // MFF has no header offset (separate files)
})
}
/// Find the data offset in a RAW file
fn find_data_offset(path: &Path, n_channels: usize, data_type: EgiDataType) -> IoResult<usize> {
let file = File::open(path)?;
let file_size = file.metadata()?.len() as usize;
// Read first 64KB to find header end
let mut reader = BufReader::new(file);
let mut buf = vec![0u8; 65536.min(file_size)];
reader.read_exact(&mut buf)?;
// Look for transition from ASCII to binary
// The header is ASCII text, data is binary
for (i, window) in buf.windows(4).enumerate() {
// Look for patterns that indicate binary data start
// In float data, we often see bytes outside ASCII range
let non_ascii_count = window.iter().filter(|&&b| b < 32 || b > 126).count();
if non_ascii_count >= 2 && i > 100 {
// Align to data type boundary
let aligned = (i / data_type.size()) * data_type.size();
return Ok(aligned);
}
}
// Fallback: estimate from file size
let data_size = n_channels * data_type.size();
if data_size > 0 && file_size > data_size {
let estimated_samples = (file_size - 1024) / data_size;
if estimated_samples > 0 {
return Ok(file_size - estimated_samples * data_size);
}
}
// Default header size
Ok(1024)
}
/// Parse info.xml from MFF
fn parse_info_xml(path: &Path) -> IoResult<(f64, usize)> {
let content = fs::read_to_string(path)?;
// Simple XML parsing for key values
let sfreq = Self::extract_xml_value(&content, "samplingRate")
.and_then(|s| s.parse().ok())
.unwrap_or(256.0);
let n_channels = Self::extract_xml_value(&content, "numberOfChannels")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
Ok((sfreq, n_channels))
}
/// Extract value from simple XML
fn extract_xml_value(content: &str, tag: &str) -> Option<String> {
let open_tag = format!("<{}>", tag);
let close_tag = format!("</{}>", tag);
if let Some(start) = content.find(&open_tag) {
let value_start = start + open_tag.len();
if let Some(end) = content[value_start..].find(&close_tag) {
return Some(content[value_start..value_start + end].trim().to_string());
}
}
None
}
/// Parse signal file to get dimensions
fn parse_signal_file(path: &Path) -> IoResult<(usize, usize, EgiDataType)> {
let metadata = fs::metadata(path)?;
let file_size = metadata.len() as usize;
// MFF signal files are typically float32
let data_type = EgiDataType::Float32;
// Read a small header to determine channel count
// MFF signal files may have a small header
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut header = [0u8; 4];
reader.read_exact(&mut header)?;
// Check if first 4 bytes look like a channel count
let possible_nchan = u32::from_le_bytes(header) as usize;
let (n_channels, header_offset) = if possible_nchan > 0 && possible_nchan < 1000 {
(possible_nchan, 4)
} else {
// Assume 256 channels as default for standard EGI nets
(256, 0)
};
let data_size = file_size - header_offset;
let n_samples = data_size / (n_channels * data_type.size());
Ok((n_samples, n_channels, data_type))
}
/// Generate default channel names
fn generate_channels(n_channels: usize, gain: f64) -> Vec<EgiChannel> {
(0..n_channels)
.map(|i| {
let (name, kind) = if i == 0 {
("Cz".to_string(), EgiChannelKind::Ref)
} else {
(format!("E{}", i), EgiChannelKind::Eeg)
};
EgiChannel {
name,
index: i,
kind,
cal: gain,
}
})
.collect()
}
/// Get duration in seconds
pub fn duration(&self) -> f64 {
self.n_samples as f64 / self.sfreq
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_egi_format_types() {
assert_eq!(EgiFormat::Raw, EgiFormat::Raw);
assert_ne!(EgiFormat::Raw, EgiFormat::Mff);
}
#[test]
fn test_channel_kind_str() {
assert_eq!(EgiChannelKind::Eeg.as_str(), "EEG");
assert_eq!(EgiChannelKind::Ref.as_str(), "REF");
assert_eq!(EgiChannelKind::Event.as_str(), "EVENT");
}
#[test]
fn test_generate_channels() {
let channels = EgiHeader::generate_channels(5, 1.0);
assert_eq!(channels.len(), 5);
assert_eq!(channels[0].name, "Cz");
assert_eq!(channels[0].kind, EgiChannelKind::Ref);
assert_eq!(channels[1].name, "E1");
assert_eq!(channels[1].kind, EgiChannelKind::Eeg);
}
#[test]
fn test_data_type_from_bytes() {
assert_eq!(EgiDataType::from_bytes(2), EgiDataType::Int16);
assert_eq!(EgiDataType::from_bytes(4), EgiDataType::Float32);
assert_eq!(EgiDataType::from_bytes(8), EgiDataType::Float64);
}
#[test]
fn test_xml_value_extraction() {
let xml = "<root><samplingRate>500</samplingRate></root>";
let value = EgiHeader::extract_xml_value(xml, "samplingRate");
assert_eq!(value, Some("500".to_string()));
let missing = EgiHeader::extract_xml_value(xml, "notFound");
assert_eq!(missing, None);
}
}
@@ -0,0 +1,46 @@
//! EGI (Electrical Geodesics, Inc.) EEG File Format Reader
//!
//! Reads data from EGI/Philips Geodesic EEG systems.
//!
//! ## File Types
//!
//! - `.raw` - Simple binary format with ASCII header
//! - `.mff` - MFF (Meta File Format) directory structure
//!
//! ## Data Format
//!
//! ### Simple RAW Format
//! The .raw format has an ASCII header followed by binary data:
//! - Header contains version, sample rate, channel count, etc.
//! - Data is big-endian float32 or int16
//!
//! ### MFF Format
//! MFF is a directory containing:
//! - `info.xml` - Session information
//! - `signal1.bin`, `signal2.bin`, ... - Binary signal files
//! - `coordinates.xml` - Sensor positions (optional)
//! - `categories.xml` - Event categories
//!
//! ## Example
//!
//! ```rust,ignore
//! use rtx_neuro_io::egi::EgiReader;
//!
//! // Read simple RAW format
//! let reader = EgiReader::open("recording.raw")?;
//! println!("Channels: {}", reader.n_channels());
//! println!("Sample rate: {} Hz", reader.sfreq());
//!
//! let data = reader.read_data(0.0, 10.0)?; // Read 10 seconds
//!
//! // Read MFF format
//! let mff_reader = EgiReader::open("recording.mff")?;
//! ```
mod constants;
mod header;
mod reader;
pub use constants::*;
pub use header::{EgiChannel, EgiChannelKind, EgiFormat, EgiHeader};
pub use reader::EgiReader;
@@ -0,0 +1,325 @@
//! EGI File Reader
//!
//! Main reader for EGI .raw and .mff files.
use crate::{IoError, IoResult, NeuroReader};
use byteorder::{BigEndian, LittleEndian, ReadBytesExt};
use std::fs::{self, File};
use std::io::{BufReader, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use super::constants::*;
use super::header::{EgiFormat, EgiHeader};
/// EGI file reader
///
/// Reads data from EGI/Philips Geodesic EEG files.
#[derive(Debug)]
pub struct EgiReader {
/// Path to the file or directory
path: PathBuf,
/// Parsed header
header: EgiHeader,
/// Channel names (cached)
channel_names: Vec<String>,
/// Signal file paths (for MFF format)
signal_files: Vec<PathBuf>,
}
impl EgiReader {
/// Open an EGI file (.raw) or directory (.mff)
pub fn open(path: impl AsRef<Path>) -> IoResult<Self> {
let path = path.as_ref();
if !path.exists() {
return Err(IoError::FileNotFound(format!(
"EGI file not found: {}",
path.display()
)));
}
let (header, signal_files) = if path.is_dir() {
// MFF directory format
let header = EgiHeader::from_mff_dir(path)?;
let signal_files = Self::find_signal_files(path)?;
(header, signal_files)
} else {
// Simple RAW format
let header = EgiHeader::from_raw_file(path)?;
let signal_files = vec![path.to_path_buf()];
(header, signal_files)
};
let channel_names = header.channels.iter().map(|c| c.name.clone()).collect();
Ok(Self {
path: path.to_path_buf(),
header,
channel_names,
signal_files,
})
}
/// Find signal files in MFF directory
fn find_signal_files(dir: &Path) -> IoResult<Vec<PathBuf>> {
let mut files = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
let name = entry.file_name().to_string_lossy().to_string();
if name.starts_with(MFF_SIGNAL_PREFIX) && name.ends_with(".bin") {
files.push(entry.path());
}
}
files.sort();
Ok(files)
}
/// Get header information
pub fn header(&self) -> &EgiHeader {
&self.header
}
/// Get path to the file/directory
pub fn path(&self) -> &Path {
&self.path
}
/// Read raw data
///
/// Returns data in channel-major format: [ch0_s0, ch0_s1, ..., ch1_s0, ...]
pub fn read_data(&mut self, tmin: f64, tmax: f64) -> IoResult<Vec<f64>> {
match self.header.format {
EgiFormat::Raw => self.read_raw_data_internal(tmin, tmax),
EgiFormat::Mff => self.read_mff_data(tmin, tmax),
}
}
/// Read data from simple RAW format
fn read_raw_data_internal(&mut self, tmin: f64, tmax: f64) -> IoResult<Vec<f64>> {
let sfreq = self.header.sfreq;
let n_channels = self.header.n_channels;
let total_samples = self.header.n_samples;
// Convert time to sample indices
let start_sample = ((tmin * sfreq).floor() as usize).min(total_samples);
let end_sample = ((tmax * sfreq).ceil() as usize).min(total_samples);
if start_sample >= end_sample {
return Ok(Vec::new());
}
let n_samples = end_sample - start_sample;
// Allocate output buffer (channel-major format)
let mut data = vec![0.0f64; n_channels * n_samples];
// Get calibration factors
let cals: Vec<f64> = self.header.channels.iter().map(|c| c.cal).collect();
// Open data file
let file = File::open(&self.signal_files[0])?;
let mut reader = BufReader::new(file);
// EGI RAW data is typically stored as: all channels for sample 0, etc.
// Data is big-endian
let sample_size = n_channels * self.header.data_type.size();
let seek_pos = self.header.header_size + start_sample * sample_size;
reader.seek(SeekFrom::Start(seek_pos as u64))?;
// Read samples based on data type
match self.header.data_type {
EgiDataType::Int16 => {
for s in 0..n_samples {
for ch in 0..n_channels {
let raw = reader.read_i16::<BigEndian>()?;
data[ch * n_samples + s] = raw as f64 * cals[ch];
}
}
}
EgiDataType::Float32 => {
for s in 0..n_samples {
for ch in 0..n_channels {
let raw = reader.read_f32::<BigEndian>()?;
data[ch * n_samples + s] = raw as f64 * cals[ch];
}
}
}
EgiDataType::Float64 => {
for s in 0..n_samples {
for ch in 0..n_channels {
let raw = reader.read_f64::<BigEndian>()?;
data[ch * n_samples + s] = raw * cals[ch];
}
}
}
}
Ok(data)
}
/// Read data from MFF format
fn read_mff_data(&mut self, tmin: f64, tmax: f64) -> IoResult<Vec<f64>> {
let sfreq = self.header.sfreq;
let n_channels = self.header.n_channels;
let total_samples = self.header.n_samples;
// Convert time to sample indices
let start_sample = ((tmin * sfreq).floor() as usize).min(total_samples);
let end_sample = ((tmax * sfreq).ceil() as usize).min(total_samples);
if start_sample >= end_sample {
return Ok(Vec::new());
}
let n_samples = end_sample - start_sample;
// Allocate output buffer (channel-major format)
let mut data = vec![0.0f64; n_channels * n_samples];
// MFF stores data in signal*.bin files
// Each file may contain all channels for a segment
if self.signal_files.is_empty() {
return Err(IoError::FileNotFound("No signal files found".to_string()));
}
// Read from first signal file (simplified - assumes single file)
let file = File::open(&self.signal_files[0])?;
let mut reader = BufReader::new(file);
// MFF signal files are typically little-endian float32
let sample_size = n_channels * self.header.data_type.size();
let seek_pos = start_sample * sample_size;
reader.seek(SeekFrom::Start(seek_pos as u64))?;
// Read samples
match self.header.data_type {
EgiDataType::Float32 => {
for s in 0..n_samples {
for ch in 0..n_channels {
let raw = reader.read_f32::<LittleEndian>()?;
data[ch * n_samples + s] = raw as f64;
}
}
}
EgiDataType::Float64 => {
for s in 0..n_samples {
for ch in 0..n_channels {
let raw = reader.read_f64::<LittleEndian>()?;
data[ch * n_samples + s] = raw;
}
}
}
EgiDataType::Int16 => {
for s in 0..n_samples {
for ch in 0..n_channels {
let raw = reader.read_i16::<LittleEndian>()?;
data[ch * n_samples + s] = raw as f64;
}
}
}
}
Ok(data)
}
/// Get net size (channel count category)
pub fn net_size(&self) -> &'static str {
match self.header.n_channels {
n if n <= EGI_NET_32 => "32",
n if n <= EGI_NET_64 => "64",
n if n <= EGI_NET_128 => "128",
_ => "256",
}
}
}
impl NeuroReader for EgiReader {
fn read_header(&mut self) -> IoResult<()> {
// Header is already parsed in open()
Ok(())
}
fn sfreq(&self) -> f64 {
self.header.sfreq
}
fn n_channels(&self) -> usize {
self.header.n_channels
}
fn n_samples(&self) -> usize {
self.header.n_samples
}
fn channel_names(&self) -> Vec<String> {
self.channel_names.clone()
}
fn read_raw_data(&mut self, tmin: f64, tmax: f64) -> IoResult<Vec<f64>> {
self.read_data(tmin, tmax)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_to_sample_conversion() {
let sfreq: f64 = 256.0;
let tmin: f64 = 0.5;
let tmax: f64 = 1.5;
let total_samples: usize = 1000;
let start_sample = ((tmin * sfreq).floor() as usize).min(total_samples);
let end_sample = ((tmax * sfreq).ceil() as usize).min(total_samples);
assert_eq!(start_sample, 128);
assert_eq!(end_sample, 384);
}
#[test]
fn test_sample_size_calculation() {
let n_channels = 128;
let int16_size = n_channels * EgiDataType::Int16.size();
assert_eq!(int16_size, 128 * 2);
let float32_size = n_channels * EgiDataType::Float32.size();
assert_eq!(float32_size, 128 * 4);
}
#[test]
fn test_net_size_detection() {
// Helper to create minimal reader for testing
fn net_size_for_channels(n: usize) -> &'static str {
match n {
n if n <= EGI_NET_32 => "32",
n if n <= EGI_NET_64 => "64",
n if n <= EGI_NET_128 => "128",
_ => "256",
}
}
assert_eq!(net_size_for_channels(32), "32");
assert_eq!(net_size_for_channels(64), "64");
assert_eq!(net_size_for_channels(128), "128");
assert_eq!(net_size_for_channels(256), "256");
assert_eq!(net_size_for_channels(65), "128");
}
#[test]
fn test_channel_major_indexing() {
let n_channels = 4;
let n_samples = 100;
// Channel 2, sample 50
let ch = 2;
let s = 50;
let idx = ch * n_samples + s;
assert_eq!(idx, 250);
}
}