Expand description
edf-rs is a pure Rust library for reading and writing EDF/EDF+ files. It aims at providing a feature-rich and
reliable way to work with *.edf files. This crate is optimized to work efficiently with very large EDF file sizes.
It is based on the official specification here. Currently the primary
focus for this project is to power my other project NoctiG Scorer.
This library is an unofficial implementation. It is still in an early development stage which is not yet considered stable and will have breaking changes in the future.
§Examples
To get started using this crate, follow the examples below. It will outline how to create and read a basic EDF+ file with some test data. To see all available fields and functions, take a look at the individual module documentations.
§Create an EDF+ file
The following example shows how to create a new EDF+ file and fill it with test data. This will
generate a file called recording.edf. It will have 1 regular signal and 1 annotations signal.
The regular signal will contain 100 samples with values from 0 to 99 in every data-record. The
annotations signal will have a time-keeping annotation and an additional record-global annotation.
use chrono::{NaiveDate, NaiveTime};
use edf_rs::EDFSpecifications;
use edf_rs::file::EDFFile;
use edf_rs::record::Record;
use edf_rs::headers::patient::{PatientId, Sex};
use edf_rs::headers::recording::RecordingId;
use edf_rs::headers::signal_header::SignalHeader;
use edf_rs::headers::annotation_list::AnnotationList;
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load the EDF+ file from any path
let mut edf = EDFFile::new("recording.edf")?;
// Configure the header of the EDF file
edf.header
.with_specification(EDFSpecifications::EDFPlus)
.with_is_continuous(true)
.with_patient_id(PatientId {
code: Some("PAT-CODE1".to_string()),
name: Some("PAT-NAME".to_string()),
date: Some(NaiveDate::from_ymd_opt(2001, 07, 11).unwrap()),
sex: Some(Sex::Male),
additional: Vec::new(),
})
.with_recording_id(RecordingId {
admin_code: Some("REC-CODE1".to_string()),
equipment: Some("EQUIPMENT".to_string()),
technician: Some("TECHNICIAN".to_string()),
startdate: Some(NaiveDate::from_ymd_opt(2026, 02, 13).unwrap()),
additional: Vec::new()
})
.with_start_date(NaiveDate::from_ymd_opt(2026, 02, 13).unwrap())
.with_start_time(NaiveTime::from_hms_opt(17, 30, 0).unwrap())
.with_record_duration(1.0);
// Create a regular signal
let mut signal = SignalHeader::new();
signal.with_label("Signal".to_string())
.with_transducer("AgAgCl cup electrodes".to_string())
.with_physical_dimension("uV".to_string())
.with_physical_range(-440.0, 510.0)
.with_digital_range(-2048, 2047)
.with_samples_count(100);
// Insert the regular and the annotation signals
edf.insert_signal(0, signal).unwrap();
edf.insert_signal(1, SignalHeader::new_annotation(80)).unwrap();
// Insert some data-records
edf.append_record(generate_record(&edf, 0)).unwrap();
edf.append_record(generate_record(&edf, 1)).unwrap();
edf.append_record(generate_record(&edf, 2)).unwrap();
edf.append_record(generate_record(&edf, 3)).unwrap();
edf.append_record(generate_record(&edf, 4)).unwrap();
// Save the file
edf.save().unwrap();
Ok(())
}
fn generate_record(edf: &EDFFile, index: usize) -> Record {
let mut record = edf.header.create_record();
record.raw_signal_samples = vec![
(0..100).collect()
];
record.annotations = vec![vec![
AnnotationList::new(0.0 + index as f64, 0.0, vec![
"".to_string(),
format!("GlobalAnnotation {}", index)
]).unwrap()
]];
record
}§Read an EDF+ file
The following example shows how to read an EDF+ file from the beginning to the end. It will read the EDF file created with the example above. It will read all data-records from the file and print the header, all annotations and the maximum values of each signal in every data-record to the console.
use edf_rs::file::EDFFile;
pub fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load the EDF+ file from any path
let mut edf = EDFFile::open("recording.edf")?;
// Print the debug output for the parsed EDF+ file header
println!("{:#?}", edf.header);
// Read all data-records and print the stored annotations
let record_count = edf.header.get_record_count().unwrap_or(0);
for i in 0..record_count {
let Some(record) = edf.read_record()? else {
continue;
};
// Print the annotations (including the time keeping annotation).
// For regular EDF files, these annotations will always be empty
// as they are only included in the EDF+ specification
println!("Annotations in data-record {}", i);
println!("{:#?}", record.annotations);
// Do something with the signals. The order of the signals
// is the same as the signals in the header
let max_signal_values: Vec<i16> = record.raw_signal_samples.into_iter()
.map(|samples| samples.into_iter().max().unwrap_or(0))
.collect();
println!("Max values: {:?}", max_signal_values)
}
Ok(())
}Further examples will be added in the future