rusty_brain/read/
brainvision_core.rs

1// * https://www.brainproducts.com/download/specification-of-brainvision-core-data-format-1-0/
2
3use core::{f32, str};
4use std::{fs, io::Read, path::Path, str::Split, u32};
5
6use ndarray::{Array2, ArrayView1};
7
8use super::BIDSPath;
9
10mod locked {
11    pub(crate) trait Locked {}
12
13    impl Locked for f32 {}
14    impl Locked for i16 {}
15}
16
17pub struct Header {
18    data_file: String,
19    marker_file: String,
20    num_channels: u32,
21    sampling_interval: f64,
22    averaged: bool,
23    averaged_segms: u32,
24    num_data_points: u32,
25    segmentation_type: String,
26    binary_format: BinaryFormatType,
27    channels: Vec<ChannelInfo>,
28    channel_coords: Option<Vec<Coordinates>>,
29    comment: Option<String>,
30}
31
32impl Header {
33    pub fn load<P: AsRef<Path>>(
34        path: &BIDSPath<P>,
35        task: &str,
36        acquisition: Option<&str>,
37        run: Option<&str>,
38    ) -> Header {
39        let mut buf = String::new();
40        let _ = fs::File::open(path.path.join(format!(
41            "sub-{}{}_task-{}{}{}_{}.vhdr",
42            path.subject,
43            if let Some(session) = path.session {
44                format!("_ses-{}", session)
45            } else {
46                String::default()
47            },
48            task,
49            if let Some(acquisition) = acquisition {
50                format!("_acq-{}", acquisition)
51            } else {
52                String::default()
53            },
54            if let Some(run) = run {
55                format!("_run-{}", run)
56            } else {
57                String::default()
58            },
59            path.datatype
60        )))
61        .unwrap()
62        .read_to_string(&mut buf);
63        // Extract the `[Comment]` section
64        let comment = buf
65            .match_indices("[Comment]")
66            .next()
67            .map(|(idx, _)| buf[idx + "[Comment]".len()..buf.len()].to_string());
68        // And skip the first line (identification line)
69        buf = buf.lines().skip(1).collect::<Vec<&str>>().join("\n");
70
71        let file = ini::Ini::load_from_str(&buf).unwrap();
72
73        let common_infos = file.section(Some("Common Infos")).unwrap();
74        let binary_infos = file.section(Some("Binary Infos")).unwrap();
75        let channel_infos = file.section(Some("Channel Infos")).unwrap();
76        let coordinates = file.section(Some("Coordinates"));
77
78        let data_file = common_infos.get("DataFile").unwrap().into();
79        let marker_file = common_infos.get("MarkerFile").unwrap().into();
80        let num_channels = common_infos
81            .get("NumberOfChannels")
82            .map(|s| s.parse::<u32>().unwrap())
83            .unwrap();
84        let sampling_interval = common_infos
85            .get("SamplingInterval")
86            .map(|s| s.parse::<f64>().unwrap())
87            .unwrap();
88        let averaged = common_infos.get("Averaged").map_or_else(
89            || false,
90            |s| match s {
91                "YES" => true,
92                "NO" | _ => false,
93            },
94        );
95        let averaged_segms = match averaged {
96            false => 0u32,
97            true => common_infos
98                .get("AveragedSegments")
99                .map(|s| s.parse::<u32>().unwrap())
100                .unwrap(),
101        };
102        let segmentation_type = match averaged {
103            false => "NOTSEGMENTED",
104            true => common_infos.get("SegmentationType").unwrap(),
105        }
106        .into();
107        let num_data_points = if averaged && segmentation_type == "MARKERBASED" {
108            common_infos
109                .get("SegmentDataPoints")
110                .map(|s| s.parse::<u32>().unwrap())
111                .unwrap()
112        } else {
113            0
114        };
115
116        let binary_format = match binary_infos.get("BinaryFormat").unwrap() {
117            "IEEE_FLOAT_32" => BinaryFormatType::IeeeFloat32,
118            "INT_16" => BinaryFormatType::Int16,
119            _ => panic!("Invalid binary format !"),
120        };
121
122        let channels = channel_infos
123            .iter()
124            .map(|(_, v)| ChannelInfo::from(v.split(',')))
125            .collect::<Vec<ChannelInfo>>();
126
127        let channel_coords = coordinates.map(|coords| {
128            coords
129                .iter()
130                .map(|(_, v)| Coordinates::from(v.split(',')))
131                .collect::<Vec<Coordinates>>()
132        });
133
134        Header {
135            data_file,
136            marker_file,
137            num_channels,
138            sampling_interval,
139            averaged,
140            averaged_segms,
141            num_data_points,
142            segmentation_type,
143            binary_format,
144            channels,
145            channel_coords,
146            comment,
147        }
148    }
149
150    pub fn data_file(&self) -> &str {
151        &self.data_file
152    }
153
154    pub fn num_channels(&self) -> u32 {
155        self.num_channels
156    }
157}
158
159pub(crate) trait BinaryFormat: locked::Locked + Sized {
160    const BYTES: usize;
161
162    fn from_bytes(bytes: &[u8]) -> Self;
163}
164
165impl BinaryFormat for f32 {
166    const BYTES: usize = 4;
167
168    fn from_bytes(bytes: &[u8]) -> Self {
169        let mut rep = [0u8; 4];
170        rep.copy_from_slice(bytes);
171        f32::from_le_bytes(rep)
172    }
173}
174
175impl BinaryFormat for i16 {
176    const BYTES: usize = 2;
177
178    fn from_bytes(bytes: &[u8]) -> Self {
179        let mut rep = [0u8; 2];
180        rep.copy_from_slice(bytes);
181        i16::from_le_bytes(rep)
182    }
183}
184
185#[derive(Clone, Copy)]
186pub enum BinaryFormatType {
187    IeeeFloat32,
188    Int16,
189}
190
191struct ChannelInfo {
192    name: String,
193    ref_name: String,
194    resolution: f64,
195    unit: String,
196}
197
198impl From<Split<'_, char>> for ChannelInfo {
199    fn from(mut value: Split<char>) -> Self {
200        let name = value.next().unwrap().into();
201        let ref_name = value
202            .next()
203            .map(|s| if s.is_empty() { "Cz" } else { s })
204            .unwrap()
205            .into();
206        let resolution = value
207            .next()
208            .map(|s| {
209                if s.is_empty() {
210                    1.0f64
211                } else {
212                    s.parse::<f64>().unwrap()
213                }
214            })
215            .unwrap();
216        let unit = value
217            .next()
218            .map(|s| if s.is_empty() { "μV" } else { s })
219            .unwrap()
220            .into();
221
222        Self {
223            name,
224            ref_name,
225            resolution,
226            unit,
227        }
228    }
229}
230
231struct Coordinates {
232    radius: f64,
233    theta: f64,
234    phi: f64,
235}
236
237impl From<Split<'_, char>> for Coordinates {
238    fn from(mut value: Split<'_, char>) -> Self {
239        Coordinates {
240            radius: value.next().map(|s| s.parse::<f64>().unwrap()).unwrap(),
241            theta: value.next().map(|s| s.parse::<f64>().unwrap()).unwrap(),
242            phi: value.next().map(|s| s.parse::<f64>().unwrap()).unwrap(),
243        }
244    }
245}
246
247#[allow(private_bounds)]
248pub struct Data<T: BinaryFormat> {
249    data: Array2<T>,
250}
251
252#[allow(private_bounds)]
253impl<T: BinaryFormat> Data<T> {
254    pub fn load<P: AsRef<Path>>(path: &BIDSPath<P>, header: &Header) -> Data<T> {
255        let rawdata = fs::read(path.path.join(header.data_file())).unwrap();
256        let num_channels = header.num_channels() as usize;
257        let chunks = rawdata.chunks_exact(T::BYTES);
258        // ! Actual number of data points is `effective_data_points` + 1
259        let effective_data_points = chunks.len() / num_channels;
260        let data = Array2::from_shape_vec(
261            (num_channels, effective_data_points),
262            chunks.map(|c| T::from_bytes(c)).collect::<Vec<T>>(),
263        )
264        .unwrap();
265
266        Data { data }
267    }
268
269    pub fn channel(&self, index: usize) -> ArrayView1<T> {
270        self.data.column(index)
271    }
272}