go-edflib - Go 语言 EDF/BDF 读写库
Teuniz EDFlib 的 Go 语言绑定,支持 EDF/EDF+/BDF/BDF+ 四种格式的读取与 EDF+/BDF+ 写入,提供 idiomatic 的 Reader/Writer API 与注释事件解析。
简介
在脑电(EEG)数据工程中,EDF(European Data Format)及其扩展格式 EDF+、BDF、BDF+ 是最广泛使用的 interchange 格式之一。无论是临床归档、多中心数据整合,还是算法流水线中的读写环节,对 EDF 族格式的可靠支持都是基础设施。
go-edflib 是 Teuniz EDFlib 的 Go 语言绑定,通过 CGO 调用 vendored 的 edflib/edflib.c,提供 idiomatic 的 Go API。它可以读取EDF/EDF+/BDF/BDF+四种格式,写入 EDF+/BDF+,并完整支持注释(annotation)/事件通道。
为什么需要 go-edflib
Go 在服务端、CLI 工具和数据管道中越来越常见,但 EDF 生态长期被 C/Python/MATLAB 主导。现有方案各有局限:
| 方案 | 局限 |
|---|---|
| 纯 Go 手写解析 | EDF 头结构、TAL 注释通道、BDF 24-bit 编码细节繁琐,易出兼容性问题 |
| Python pyEDFlib | 适合脚本与科研,但不适合编译为单一二进制、高并发服务 |
| 直接调用 C EDFlib | 需要手动管理 handle、字符串转换、错误码,开发体验差 |
go-edflib 在 EDFlib 的稳定性与 Go 的工程化之间取平衡:底层复用经过广泛验证的 EDFlib,上层封装 Header、SignalHeader、Annotation、Reader、Writer 等类型,让 Go 项目能以自然的方式读写脑电文件。
支持的格式
读取 — 四种格式均可
| 格式 | 常量 | 位深 | 注释 |
|---|---|---|---|
| Classic EDF | FileTypeEDF | 16-bit | 无标准注释通道 |
| EDF+ | FileTypeEDFPlus | 16-bit | 含 TAL 注释 |
| Classic BDF | FileTypeBDF | 24-bit | BioSemi 格式 |
| BDF+ | FileTypeBDFPlus | 24-bit | 含 TAL 注释 |
写入 — 仅 EDF+ / BDF+
这是 EDFlib 上游的设计限制,而非 go-edflib 刻意收窄能力。EDFlib 的 edfopen_file_writeonly 只接受 EDF+ 和 BDF+,因为写入 API 围绕结构化 patient/recording 字段、自动注释通道和 WriteAnnotation(TAL)构建,Classic EDF/BDF 使用单一 80 字节 patient/recording 字符串且无标准注释通道。
| 目标 | 推荐做法 |
|---|---|
| 新建带注释的录制 | 写入 FileTypeEDFPlus 或 FileTypeBDFPlus |
| 与 EDFbrowser 等工具互操作 | EDF+ 已被广泛支持,可作为默认 interchange 格式 |
| Classic → Plus 转换 | OpenRead 读取原文件,CreateWriter 写入 Plus,复制头字段与样本 |
| 必须输出 Classic EDF/BDF | EDFlib 不支持,需其他库或自行实现固定头布局 |
核心 API
Reader — 读取文件
import (
"fmt"
"github.com/eegdb/go-edflib"
)
r, err := edflib.OpenRead("recording.edf", true) // true = 加载全部注释
if err != nil {
panic(err)
}
defer r.Close()
hdr := r.Header()
fmt.Println(hdr.NumSignals, hdr.FileDurationSec)
// 读取通道 0 在 1s–2s 的物理量样本(秒,相对录制起点)
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)
}Reader 提供多种读取模式,适应不同场景:
- 顺序按 data record 读取 —
ReadRecordDigital(sig),须按 record 遍历,每个 record 内 signal 0 → N−1 顺序读取 - 按时间区间随机访问 —
ReadPhysicalInterval/ReadDigitalInterval,适合截取片段 - 按样本索引随机访问 —
ReadPhysicalAt/ReadDigitalAt - 仅读头信息 —
ReadHeader(path, readAllAnnotations),打开后立即关闭,适合元数据索引
Writer — 写入 EDF+
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, // 默认 1s data record 时 = 采样率 Hz
SampleRate: 256,
})
samples := make([]int16, 256)
// 填充样本...
w.WriteRecord([][]int16{samples})
w.WriteAnnotation(1_000_000, 0, "stimulus") // onset µs, duration µs (0 = 瞬时)Writer 支持 EDF+ 结构化元数据:SetPatientCode、SetPatientName、SetSex、SetBirthdate、SetTechnician、SetEquipment 等。注释可通过 WriteAnnotation 写入 UTF-8 TAL 事件。
关键类型
Header
文件级元数据,由 Reader.Header() 或 ReadHeader() 返回:
| 字段 | 说明 |
|---|---|
FileType | EDF / EDF+ / BDF / BDF+ |
NumSignals | 数据通道数量(不含注释通道) |
DataRecords | data record 总数 |
DataRecordDurSec | 单个 data record 时长(秒) |
FileDurationSec | 录制总时长 |
StartTime | 录制起始时间(UTC) |
PatientCode, PatientName, Sex, BirthDate | EDF+ 患者字段 |
AdminCode, Technician, Equipment | EDF+ 录制字段 |
Signals | 各通道 SignalHeader |
Annotations | 解析后的 EDF+/BDF+ 事件 |
SignalHeader
| 字段 | 说明 |
|---|---|
Label, Transducer, PhysicalDim, Prefilter | 通道元数据 |
PhysicalMin, PhysicalMax | 物理量范围 |
DigitalMin, DigitalMax | 数字量范围 |
SamplesPerRecord | 每个 data record 的样本数 |
SampleRate | SamplesPerRecord / DataRecordDurSec |
TotalSamples | 文件中该通道总样本数 |
Annotation
| 字段 | 说明 |
|---|---|
OnsetUs | 相对录制起点的 onset(微秒) |
DurationUs | 持续时间(微秒),0 表示瞬时事件 |
Text | UTF-8 描述文本 |
使用注意事项
以下行为来自 EDFlib 规范,在使用 go-edflib 时需要留意:
| 主题 | 行为 |
|---|---|
| 信号索引 | NumSignals 与 signal[i] 仅指数据通道;EDF+/BDF+ 的注释通道不计入 |
| 注释通道 | 原始文件中 EDF Annotations / BDF Annotations 标签出现在最后一个 signal,不要当作 EEG 通道索引 |
| Data record | 默认时长 1 秒;此时 SamplesPerRecord = 采样率(Hz) |
| 顺序读取 | ReadRecordDigital 须对每个 record 按 signal 0, 1, …, N−1 顺序读取 |
| BDF 样本 | ReadDigital 返回 24-bit 值为 int32;ReadRecordDigital 为便利起见 clamp 到 int16 |
| 写入格式 | CreateWriter 仅创建 EDF+ / BDF+;传入 Classic 类型会返回 edflib: file type error |
技术架构
┌─────────────────────────────────────────┐
│ Go Application │
│ (EEGDB import/export, CLI, services) │
└─────────────────┬───────────────────────┘
│ idiomatic API
┌─────────────────▼───────────────────────┐
│ go-edflib │
│ Reader / Writer / Header / Annotation │
│ reader.go writer.go edflib.go cgo.go│
└─────────────────┬───────────────────────┘
│ CGO
┌─────────────────▼───────────────────────┐
│ edflib/edflib.c (vendored) │
│ Teuniz EDFlib (BSD-3-Clause) │
└─────────────────────────────────────────┘
- CGO 绑定:通过
cgo.go桥接 Go 与 C,vendored 的edflib/目录确保版本锁定、无需系统级安装 EDFlib - 类型安全:Go 侧将 C 结构体映射为
Header、SignalHeader等,避免调用方直接操作 raw handle - 注释模式:
ReadAnnotationMode支持跳过、按需读取或一次性加载全部注释,平衡内存与便利性
环境与安装
要求:
- Go 1.23+
- C 编译器(
gcc或clang) CGO_ENABLED=1
# Debian/Ubuntu
sudo apt install build-essential
export CGO_ENABLED=1
# 安装
go get github.com/eegdb/go-edflib运行测试:
CGO_ENABLED=1 go test ./test/...
# 检查任意 EDF/BDF 文件(头信息 + 通道 0 的 1s–2s 物理样本)
EDF_FILE=/path/to/file.edf go test ./test -run TestInspectEDF -v应用场景
高性能 EDF 处理服务
对于需要并发处理大量 EDF 文件的场景(批量元数据索引、格式校验、Classic → Plus 转换),Go 的 goroutine 模型配合 go-edflib 的随机访问 API(ReadPhysicalInterval)可以高效截取片段而无需顺序扫描整个文件。
CLI 与边缘部署
编译为静态链接(或 mostly-static)CLI 工具后,可在无 Python 环境的机器上直接 inspect、转换 EDF 文件,适合实验室工作站、数据采集端或 CI 流水线中的格式检查步骤。
许可证
| 组件 | 许可证 |
|---|---|
| Go wrapper | MIT |
edflib/edflib.c, edflib.h | BSD-3-Clause(Teunis van Beelen) |
项目地址
- GitHub: https://github.com/eegdb/go-edflib
- 上游 EDFlib: https://www.teuniz.net/edflib/
- EDF+ 规范: https://www.edfplus.info/