Go bindings for Teuniz EDFlib — read and write EDF, EDF+, BDF, BDF+, including annotations/events.
Standalone library (cgo). EEGDB uses it for import/export.
- EDF / EDF+ (16-bit) and BDF / BDF+ (24-bit) via vendored
edflib/edflib.c - Read: header metadata, per-record or random-access samples, time-range queries, annotations
- Write: EDF+/BDF+ with patient/recording fields, multi-channel data records, UTF-8 annotations
- Idiomatic Go types:
Header,SignalHeader,Annotation,Reader,Writer
- Go 1.23+
- C compiler (
gccorclang) CGO_ENABLED=1
sudo apt install build-essential # Debian/Ubuntu
export CGO_ENABLED=1go get github.com/eegdb/go-edflibLocal development:
# In your go.mod
replace github.com/eegdb/go-edflib => ../go-edflibimport (
"fmt"
"github.com/eegdb/go-edflib"
)
r, err := edflib.OpenRead("recording.edf", true) // true = load all annotations
if err != nil {
panic(err)
}
defer r.Close()
hdr := r.Header()
fmt.Println(hdr.NumSignals, hdr.FileDurationSec)
// Physical samples on channel 0, from 1s to 2s (seconds from recording start)
phys, err := r.ReadPhysicalInterval([]int{0}, 1.0, 2.0)
if err != nil {
panic(err)
}
_ = phys[0]
for _, a := range hdr.Annotations {
fmt.Println(a.OnsetUs, a.Text)
}import (
"time"
"github.com/eegdb/go-edflib"
)
w, err := edflib.CreateWriter("out.edf", edflib.FileTypeEDFPlus, 1)
if err != nil {
panic(err)
}
defer w.Close()
w.SetStartTime(time.Now().UTC())
w.SetPatientCode("P001")
w.ConfigureSignal(0, edflib.SignalHeader{
Label: "EEG",
PhysicalDim: "uV",
PhysicalMin: -500,
PhysicalMax: 500,
DigitalMin: -32768,
DigitalMax: 32767,
SamplesPerRecord: 256, // per 1s data record (default duration)
SampleRate: 256,
})
samples := make([]int16, 256)
// fill samples...
w.WriteRecord([][]int16{samples})
w.WriteAnnotation(1_000_000, 0, "stimulus") // onset µs, duration µs (0 = instant)| Topic | Behavior |
|---|---|
| Writing | CreateWriter / CreateWriterWithParams only create EDF+ or BDF+ files (EDFlib limitation). Classic EDF/BDF are read-only here. |
| Signal index | NumSignals and signal[i] are data channels only; EDF+/BDF+ annotation channels are excluded from the count. |
| Annotation channel | In the raw file, labels like EDF Annotations / BDF Annotations appear as the last signal; do not use them as EEG indices. |
| Data records | Default record duration is 1 second. SamplesPerRecord = sample rate (Hz) when duration is 1s. |
| Read order | ReadRecordDigital must read signals 0, 1, …, N−1 in order for each data record. |
| Write order | WriteRecord writes one record: one buffer per data channel, in signal order. |
| BDF samples | ReadDigital returns 24-bit values as int32; ReadRecordDigital clamps to int16 for convenience. |
EDFlib (and therefore go-edflib) treats reading and writing differently.
OpenRead / ReadHeader accept any file EDFlib recognizes:
FileType |
Format | Bit depth |
|---|---|---|
FileTypeEDF |
Classic EDF | 16-bit |
FileTypeEDFPlus |
EDF+ | 16-bit + annotations |
FileTypeBDF |
Classic BDF | 24-bit |
FileTypeBDFPlus |
BDF+ | 24-bit + annotations |
CreateWriter calls EDFlib’s edfopen_file_writeonly, which rejects classic EDF/BDF:
// edflib/edflib.c — only these two types are allowed
if (filetype != EDFLIB_FILETYPE_EDFPLUS && filetype != EDFLIB_FILETYPE_BDFPLUS)
return EDFLIB_FILETYPE_ERROR;Passing FileTypeEDF or FileTypeBDF to CreateWriter returns edflib: file type error. This is an upstream EDFlib design choice, not a go-edflib restriction.
Why EDFlib does this
- The write API is built around EDF+/BDF+ features: structured patient/recording fields (
SetPatientCode,SetRecordingAdditional, …), automatic annotation channel(s), andWriteAnnotation(TAL). Classic EDF/BDF use a single 80-byte patient/recording string and have no standard annotation channel. - There is no
edfopen_file_writeonlypath that emits byte-accurate classic EDF or BDF from scratch.
What to do in practice
| Goal | Approach |
|---|---|
| New recording with annotations | Write FileTypeEDFPlus or FileTypeBDFPlus |
| Tool compatibility | EDF+ is widely opened by EDFbrowser and similar tools; many workflows treat it as the default interchange format |
| Convert classic → plus | OpenRead the classic file, CreateWriter as EDF+/BDF+, copy header fields and samples (see test/edflib_test.go exportEDF) |
| Must output classic EDF/BDF only | Not supported by EDFlib’s writer; use another library or implement the fixed header layout yourself |
| Constant | Meaning |
|---|---|
FileTypeEDF |
Classic EDF (16-bit), read |
FileTypeEDFPlus |
EDF+ with annotations |
FileTypeBDF |
Classic BDF (24-bit), read |
FileTypeBDFPlus |
BDF+ with annotations |
File-level metadata returned by Reader.Header() / ReadHeader().
| Field | Description |
|---|---|
FileType |
EDF / EDF+ / BDF / BDF+ |
NumSignals |
Number of data signals (no annotation channel) |
DataRecords |
Number of data records in file |
DataRecordDurSec |
Duration of one data record (seconds) |
FileDurationSec |
Total recording length (seconds) |
StartTime |
Recording start (UTC) |
Patient, Recording |
Legacy EDF fields |
PatientCode, PatientName, Sex, BirthDate, PatientAdditional |
EDF+ patient fields |
AdminCode, Technician, Equipment, RecordingAdditional |
EDF+ recording fields |
Signals |
Per-channel SignalHeader slice |
Annotations |
Parsed EDF+/BDF+ events (if loaded) |
| Field | Description |
|---|---|
Label, Transducer, PhysicalDim, Prefilter |
Channel metadata strings |
PhysicalMin, PhysicalMax |
Physical value range |
DigitalMin, DigitalMax |
Stored digital range |
SamplesPerRecord |
Samples per data record for this channel |
SampleRate |
SamplesPerRecord / DataRecordDurSec |
TotalSamples |
Total samples in file for this channel |
| Field | Description |
|---|---|
OnsetUs |
Microseconds from recording start |
DurationUs |
Duration in µs; 0 if instantaneous |
Text |
UTF-8 description |
| Constant | Description |
|---|---|
DoNotReadAnnotations |
Skip annotation parsing |
ReadAnnotations |
Read annotations (default for OpenRead(..., false)) |
ReadAllAnnotations |
Read all annotations (OpenRead(..., true)) |
| Name | Description |
|---|---|
MaxSignals |
Max data signals per file (EDFlib limit) |
MaxAnnotationLen |
Max UTF-8 annotation text length |
EDF16DigitalMin, EDF16DigitalMax |
Standard 16-bit EDF digital range |
SeekSet, SeekCur, SeekEnd |
Reader.Seek whence values |
AnnotChanIdxPosEnd, AnnotChanIdxPosMiddle, AnnotChanIdxPosStart |
Annotation channel placement when writing |
SexMale, SexFemale |
Writer.SetSex values |
| Function | Description |
|---|---|
Version() |
Linked EDFlib version number |
IsBDF(ft FileType) bool |
Whether file type uses 24-bit BDF samples |
IsFileUsed(path string) bool |
Whether path is open in EDFlib |
NumberOfOpenFiles() int |
Count of open EDFlib handles |
HandleAt(fileNumber int) int |
EDFlib handle for N-th open file, or -1 |
EDF16DigitalRange(digMin, digMax int) (int, int) |
Clamp/validate digital range for 16-bit EDF |
ClampInt16(v int32) int16 |
Clamp 24-bit value to int16 |
Open with OpenRead or OpenReadWithMode. Always Close() when done.
| Function | Description |
|---|---|
OpenRead(path, readAllAnnotations bool) (*Reader, error) |
Open file; true → ReadAllAnnotations |
OpenReadWithMode(path, mode ReadAnnotationMode) (*Reader, error) |
Open with explicit annotation mode |
ReadHeader(path, readAllAnnotations bool) (Header, error) |
Read header only (opens and closes file) |
ReadHeaderWithMode(path, mode) (Header, error) |
Header with explicit annotation mode |
(r *Reader) Header() Header |
Cached file metadata |
(r *Reader) Close() error |
Close file |
(r *Reader) Annotation(n int) (Annotation, error) |
Get n-th annotation by index |
(r *Reader) Seek(sig, offset, whence) (int64, error) |
Set sample position for one signal |
(r *Reader) Tell(sig int) (int64, error) |
Current sample index |
(r *Reader) Rewind(sig int) error |
Seek signal to start |
(r *Reader) ReadDigital(sig, n int) ([]int32, error) |
Read n raw digital samples from current position |
(r *Reader) ReadPhysical(sig, n int) ([]float64, error) |
Read n physical-unit samples |
(r *Reader) ReadDigitalAt(sig, startSample, n int) ([]int32, error) |
Seek then read digital |
(r *Reader) ReadPhysicalAt(sig, startSample, n int) ([]float64, error) |
Seek then read physical |
(r *Reader) ReadDigitalInterval(signals []int, startSec, endSec float64) ([][]int32, error) |
Time-range digital read (per channel) |
(r *Reader) ReadPhysicalInterval(signals []int, startSec, endSec float64) ([][]float64, error) |
Time-range physical read |
(r *Reader) ReadRecordDigital(sig int) ([]int16, error) |
One data record for one signal; read sig 0..N−1 per record |
Sequential (by data record) — required order: for each record, signal 0, then 1, …:
hdr := r.Header()
for rec := int64(0); rec < hdr.DataRecords; rec++ {
for sig := 0; sig < hdr.NumSignals; sig++ {
samples, err := r.ReadRecordDigital(sig)
// ...
}
}Random access by time (recommended for snippets):
// Channel 1 (e.g. "CH1"), 1.0s ≤ t < 2.0s
data, err := r.ReadPhysicalInterval([]int{1}, 1.0, 2.0)Random access by sample index:
phys, err := r.ReadPhysicalAt(0, 250, 250) // channel 0, sample 250, count 250Create with CreateWriter or CreateWriterWithParams. Configure each signal, then write records. Close() finalizes the file.
| Function | Description |
|---|---|
CreateWriter(path, fileType, numSignals int) (*Writer, error) |
New EDF+/BDF+ file; default 1s data records |
CreateWriterWithParams(path, fileType, numSignals, sampleFreq int, physMaxMin float64, physDim string) (*Writer, error) |
Create with shared sample rate and symmetric physical range |
(w *Writer) Close() error |
Finalize and close |
(w *Writer) SetStartTime(t time.Time) error |
Recording start date/time |
(w *Writer) SetSubsecondStartTime(subsecond100ns int) error |
Sub-second offset (100 ns units) |
(w *Writer) SetPatientName, SetPatientCode, SetSex, SetBirthdate, SetPatientAdditional |
EDF+ patient fields |
(w *Writer) SetAdminCode, SetTechnician, SetEquipment, SetRecordingAdditional |
EDF+ recording fields |
(w *Writer) SetDataRecordDuration(duration10us int) error |
Record duration in 10 µs units (100000 = 1 s) |
(w *Writer) SetMicroDataRecordDuration(durationUs int) error |
Record duration 1–9999 µs |
(w *Writer) SetNumberOfAnnotationSignals(n int) error |
Annotation channel count (default 1) |
(w *Writer) SetAnnotChanIdxPos(pos int) error |
AnnotChanIdxPosEnd / Middle / Start |
(w *Writer) ConfigureSignal(edfSignal int, sig SignalHeader) error |
Per-channel header before writing data |
(w *Writer) WriteRecord(buf [][]int16) error |
One data record, all channels |
(w *Writer) WritePhysical(buf []float64) error |
One channel, physical samples, one record |
(w *Writer) BlockWritePhysical(buf []float64) error |
All channels, physical, one record |
(w *Writer) WriteDigitalShort(buf []int16) error |
One channel, 16-bit digital, one record |
(w *Writer) WriteDigital(buf []int32) error |
One channel, digital (BDF 24-bit), one record |
(w *Writer) BlockWriteDigitalShort(buf []int16) error |
All channels, 16-bit, one record |
(w *Writer) BlockWriteDigital(buf []int32) error |
All channels, digital, one record |
(w *Writer) BlockWriteDigital3Byte(buf []byte) error |
All channels, 24-bit LE bytes, one record |
(w *Writer) WriteAnnotation(onsetUs, durationUs int64, text string) error |
UTF-8 annotation |
(w *Writer) WriteAnnotationLatin1(onsetUs, durationUs int64, text string) error |
Latin-1 annotation |
Low-level write APIs (WriteDigitalShort, BlockWrite*, …) call EDFlib once per signal or once per block; prefer WriteRecord unless you need fine-grained control.
CGO_ENABLED=1 go test ./test/...Inspect any EDF/BDF file (header + channel 0 physical samples 1s–2s):
EDF_FILE=/path/to/file.edf go test ./test -run TestInspectEDF -v| Environment variable | Default | Description |
|---|---|---|
EDF_FILE |
test/testdata/source.edf |
Path to inspect |
EDF_INSPECT_MAX_ANNOT |
10 |
Max annotation lines printed; 0 = print all |
- Go wrapper: MIT (LICENSE)
edflib/edflib.c,edflib.h: BSD-3-Clause (Teunis van Beelen)