edf_rs/lib.rs
1/*!
2`edf-rs` is a pure Rust library for reading and writing EDF/EDF+ files. It aims at providing a feature-rich and
3reliable way to work with `*.edf` files. This crate is optimized to work efficiently with very large EDF file sizes.
4It is based on the official specification [here](https://www.edfplus.info/). Currently the primary
5focus for this project is to power my other project [NoctiG Scorer](https://github.com/BitFlaker/noctig-scorer).
6
7**This library is an unofficial implementation. It is still in an early development stage which is not yet
8considered stable and will have breaking changes in the future.**
9
10# Examples
11To get started using this crate, follow the examples below. It will outline how to create and read a
12basic EDF+ file with some test data. To see all available fields and functions, take a look at the
13individual module documentations.
14
15## Create an EDF+ file
16
17The following example shows how to create a new EDF+ file and fill it with test data. This will
18generate a file called `recording.edf`. It will have 1 regular signal and 1 annotations signal.
19The regular signal will contain 100 samples with values from 0 to 99 in every data-record. The
20annotations signal will have a time-keeping annotation and an additional record-global annotation.
21
22```no_run
23use chrono::{NaiveDate, NaiveTime};
24
25use edf_rs::EDFSpecifications;
26use edf_rs::file::EDFFile;
27use edf_rs::record::Record;
28use edf_rs::headers::patient::{PatientId, Sex};
29use edf_rs::headers::recording::RecordingId;
30use edf_rs::headers::signal_header::SignalHeader;
31use edf_rs::headers::annotation_list::AnnotationList;
32
33pub fn main() -> Result<(), Box<dyn std::error::Error>> {
34 // Load the EDF+ file from any path
35 let mut edf = EDFFile::new("recording.edf")?;
36
37 // Configure the header of the EDF file
38 edf.header
39 .with_specification(EDFSpecifications::EDFPlus)
40 .with_is_continuous(true)
41 .with_patient_id(PatientId {
42 code: Some("PAT-CODE1".to_string()),
43 name: Some("PAT-NAME".to_string()),
44 date: Some(NaiveDate::from_ymd_opt(2001, 07, 11).unwrap()),
45 sex: Some(Sex::Male),
46 additional: Vec::new(),
47 })
48 .with_recording_id(RecordingId {
49 admin_code: Some("REC-CODE1".to_string()),
50 equipment: Some("EQUIPMENT".to_string()),
51 technician: Some("TECHNICIAN".to_string()),
52 startdate: Some(NaiveDate::from_ymd_opt(2026, 02, 13).unwrap()),
53 additional: Vec::new()
54 })
55 .with_start_date(NaiveDate::from_ymd_opt(2026, 02, 13).unwrap())
56 .with_start_time(NaiveTime::from_hms_opt(17, 30, 0).unwrap())
57 .with_record_duration(1.0);
58
59 // Create a regular signal
60 let mut signal = SignalHeader::new();
61 signal.with_label("Signal".to_string())
62 .with_transducer("AgAgCl cup electrodes".to_string())
63 .with_physical_dimension("uV".to_string())
64 .with_physical_range(-440.0, 510.0)
65 .with_digital_range(-2048, 2047)
66 .with_samples_count(100);
67
68 // Insert the regular and the annotation signals
69 edf.insert_signal(0, signal).unwrap();
70 edf.insert_signal(1, SignalHeader::new_annotation(80)).unwrap();
71
72 // Insert some data-records
73 edf.append_record(generate_record(&edf, 0)).unwrap();
74 edf.append_record(generate_record(&edf, 1)).unwrap();
75 edf.append_record(generate_record(&edf, 2)).unwrap();
76 edf.append_record(generate_record(&edf, 3)).unwrap();
77 edf.append_record(generate_record(&edf, 4)).unwrap();
78
79 // Save the file
80 edf.save().unwrap();
81
82 Ok(())
83}
84
85fn generate_record(edf: &EDFFile, index: usize) -> Record {
86 let mut record = edf.header.create_record();
87 record.raw_signal_samples = vec![
88 (0..100).collect()
89 ];
90 record.annotations = vec![vec![
91 AnnotationList::new(0.0 + index as f64, 0.0, vec![
92 "".to_string(),
93 format!("GlobalAnnotation {}", index)
94 ]).unwrap()
95 ]];
96
97 record
98}
99```
100
101## Read an EDF+ file
102
103The following example shows how to read an EDF+ file from the beginning to the end. It will read the EDF
104file created with the example [above](#create-an-edf-file). It will read all data-records from the file and
105print the header, all annotations and the maximum values of each signal in every data-record to the console.
106
107```no_run
108use edf_rs::file::EDFFile;
109
110pub fn main() -> Result<(), Box<dyn std::error::Error>> {
111 // Load the EDF+ file from any path
112 let mut edf = EDFFile::open("recording.edf")?;
113
114 // Print the debug output for the parsed EDF+ file header
115 println!("{:#?}", edf.header);
116
117 // Read all data-records and print the stored annotations
118 let record_count = edf.header.get_record_count().unwrap_or(0);
119 for i in 0..record_count {
120 let Some(record) = edf.read_record()? else {
121 continue;
122 };
123
124 // Print the annotations (including the time keeping annotation).
125 // For regular EDF files, these annotations will always be empty
126 // as they are only included in the EDF+ specification
127 println!("Annotations in data-record {}", i);
128 println!("{:#?}", record.annotations);
129
130 // Do something with the signals. The order of the signals
131 // is the same as the signals in the header
132 let max_signal_values: Vec<i16> = record.raw_signal_samples.into_iter()
133 .map(|samples| samples.into_iter().max().unwrap_or(0))
134 .collect();
135 println!("Max values: {:?}", max_signal_values)
136 }
137
138 Ok(())
139}
140```
141
142Further examples will be added in the future
143*/
144
145pub mod error;
146pub mod file;
147pub mod headers;
148pub mod record;
149pub mod save;
150mod tests;
151pub mod utils;
152
153#[derive(Debug, Default, Clone, PartialEq)]
154pub enum EDFSpecifications {
155 /// The original EDF specification from 1992. See the official specifications [here](https://www.edfplus.info/specs/edf.html).
156 EDF,
157
158 #[default]
159 /// The extended EDF specification from 2003. See the official specifications [here](https://www.edfplus.info/specs/edfplus.html).
160 EDFPlus,
161}