///|
fn validate_signal(s : Signal, format : Format) -> Unit raise EdfError {
let low = if format == Edf { -32768 } else { -8388608 }
let high = if format == Edf { 32767 } else { 8388607 }
if s.digital_min < low ||
s.digital_max > high ||
s.digital_max <= s.digital_min {
raise Invalid("invalid digital calibration bounds")
}
if !finite(s.physical_min) ||
!finite(s.physical_max) ||
s.physical_min == s.physical_max ||
!finite(s.physical_max - s.physical_min) {
raise Invalid("invalid physical calibration bounds")
}
if s.samples_per_record < 1 || s.samples_per_record > 8388608 / format.width() {
raise Limit("samples per record out of bounds")
}
if s.is_annotation() {
if s.label !=
(if format == Edf { "EDF Annotations" } else { "BDF Annotations" }) ||
s.digital_min != low ||
s.digital_max != high {
raise Invalid("invalid annotation channel metadata")
}
}
}
///|
pub fn parse_header(data : Bytes) -> Header raise EdfError {
need(data, 0, 256)
let format = if part(data, 0, 8) == b"0 " {
Edf
} else if part(data, 0, 8) == b"\xffBIOSEMI" {
Bdf
} else {
raise Invalid("not an EDF or BDF signature")
}
let ns = integer(text_field(data, 252, 4))
if ns < 1 || ns > 4096 {
raise Limit("signal count must be 1..4096")
}
let bytes = integer(text_field(data, 184, 8))
if bytes != 256 + ns * 256 {
raise Invalid("header byte count disagrees with signal count")
}
need(data, 0, bytes)
let reserved = text_field(data, 192, 44)
let continuity = if reserved.has_prefix("EDF+C") ||
reserved.has_prefix("BDF+C") {
Continuous
} else if reserved.has_prefix("EDF+D") || reserved.has_prefix("BDF+D") {
Discontinuous
} else {
Plain
}
if (format == Edf && reserved.has_prefix("BDF+")) ||
(format == Bdf && reserved.has_prefix("EDF+")) {
raise Invalid("format and plus signature disagree")
}
let patient = text_field(data, 8, 80)
let recording = text_field(data, 88, 80)
let date = text_field(data, 168, 8)
let time = text_field(data, 176, 8)
ignore(parse_datetime(date, time))
let declared_records = integer(text_field(data, 236, 8))
if declared_records < -1 || declared_records > 1000000 {
raise Limit("record count out of range")
}
let duration = number(text_field(data, 244, 8))
if duration < 0.0 || duration > 86400.0 {
raise Limit("record duration outside 0..86400 seconds")
}
let signals = []
let offsets = []
let mut record_bytes = 0
let mut ann = 0
let mut ordinary = 0
let mut all_single = true
for i = 0; i < ns; i = i + 1 {
let signal : Signal = {
label: text_field(data, 256 + i * 16, 16),
transducer: text_field(data, 256 + ns * 16 + i * 80, 80),
unit: text_field(data, 256 + ns * 96 + i * 8, 8),
physical_min: number(text_field(data, 256 + ns * 104 + i * 8, 8)),
physical_max: number(text_field(data, 256 + ns * 112 + i * 8, 8)),
digital_min: integer(text_field(data, 256 + ns * 120 + i * 8, 8)),
digital_max: integer(text_field(data, 256 + ns * 128 + i * 8, 8)),
prefilter: text_field(data, 256 + ns * 136 + i * 80, 80),
samples_per_record: integer(text_field(data, 256 + ns * 216 + i * 8, 8)),
reserved: text_field(data, 256 + ns * 224 + i * 32, 32),
}
validate_signal(signal, format)
let size = signal.samples_per_record * format.width()
if size > 8388608 - record_bytes {
raise Limit("record exceeds 8 MiB")
}
offsets.push(record_bytes)
record_bytes += size
if signal.is_annotation() {
ann += 1
} else {
ordinary += 1
if signal.samples_per_record != 1 {
all_single = false
}
}
signals.push(signal)
}
if continuity != Plain && ann == 0 {
raise Invalid("plus recording needs an annotation channel")
}
if continuity == Plain && ann > 0 {
raise Invalid("annotation channel requires plus format marker")
}
if duration == 0.0 &&
!(continuity != Plain &&
(ordinary == 0 || (continuity == Discontinuous && all_single))) {
raise Invalid("zero duration is not allowed for this recording")
}
{
format,
continuity,
patient,
recording,
date,
time,
reserved,
declared_records,
duration,
signals,
bytes,
record_bytes,
offsets,
}
}
///|
/// Serialize a complete header, rejecting rather than truncating overwide fields.
fn make_header(
format : Format,
continuity : Continuity,
patient : String,
recording : String,
date : String,
time : String,
reserved : String,
count : Int,
duration : Double,
signals : Array[Signal],
) -> Bytes raise EdfError {
if signals.length() < 1 || signals.length() > 4096 {
raise Limit("signal count must be 1..4096")
}
let ns = signals.length()
let out = Array::make(256 + ns * 256, b' ')
copy_bytes(
out,
0,
if format == Edf {
b"0 "
} else {
b"\xffBIOSEMI"
},
0,
8,
)
put_text(out, 8, 80, patient)
put_text(out, 88, 80, recording)
put_text(out, 168, 8, date)
put_text(out, 176, 8, time)
put_text(out, 184, 8, out.length().to_string())
let marker = if continuity == Plain {
reserved
} else {
(if format == Edf { "EDF+" } else { "BDF+" }) +
(if continuity == Continuous { "C" } else { "D" })
}
put_text(
out,
192,
44,
if continuity == Plain || reserved.has_prefix(marker) {
reserved
} else {
marker
},
)
put_text(out, 236, 8, count.to_string())
put_text(out, 244, 8, duration.to_string())
put_text(out, 252, 4, ns.to_string())
for i = 0; i < ns; i = i + 1 {
let s = signals[i]
put_text(out, 256 + i * 16, 16, s.label)
put_text(out, 256 + ns * 16 + i * 80, 80, s.transducer)
put_text(out, 256 + ns * 96 + i * 8, 8, s.unit)
put_text(out, 256 + ns * 104 + i * 8, 8, s.physical_min.to_string())
put_text(out, 256 + ns * 112 + i * 8, 8, s.physical_max.to_string())
put_text(out, 256 + ns * 120 + i * 8, 8, s.digital_min.to_string())
put_text(out, 256 + ns * 128 + i * 8, 8, s.digital_max.to_string())
put_text(out, 256 + ns * 136 + i * 80, 80, s.prefilter)
put_text(out, 256 + ns * 216 + i * 8, 8, s.samples_per_record.to_string())
put_text(out, 256 + ns * 224 + i * 32, 32, s.reserved)
}
let b = Bytes::from_array(out)
ignore(parse_header(b))
b
}