///|
/// Decode complete records; unknown (-1) count is inferred only when no partial record remains.
pub fn decode(data : Bytes) -> Recording raise EdfError {
if data.length() > 268435456 {
raise Limit("input exceeds 256 MiB")
}
let h = parse_header(data)
let payload = data.length() - h.bytes
if payload % h.record_bytes != 0 {
raise Invalid("partial trailing data record")
}
let records = payload / h.record_bytes
if records > 1000000 {
raise Limit("record count exceeds one million")
}
if h.declared_records != -1 && h.declared_records != records {
raise Invalid("declared record count disagrees with file length")
}
let starts = []
let annotations = []
let annotation_only = h.signals.all(s => s.is_annotation())
for r = 0; r < records; r = r + 1 {
let mut onset = r.to_double() * h.duration
let mut first = true
for s = 0; s < h.signals.length(); s = s + 1 {
let signal = h.signals[s]
if !signal.is_annotation() {
continue
}
let bytes = part(
data,
h.bytes + r * h.record_bytes + h.offsets[s],
signal.samples_per_record * h.format.width(),
)
let tals = parse_tals(bytes)
if first {
if tals.is_empty() ||
tals[0].texts.is_empty() ||
tals[0].texts[0] != "" ||
tals[0].duration is Some(_) {
raise Invalid("missing first timekeeping TAL")
}
onset = tal_number(tals[0].onset, true)
if annotation_only &&
(tals[0].texts.length() < 2 || tals[0].texts[1].is_empty()) {
raise Invalid(
"annotation-only record needs an event in its first TAL",
)
}
first = false
}
for tal in tals {
let time = tal_number(tal.onset, true)
let duration = match tal.duration {
Some(d) => Some(tal_number(d, false))
None => None
}
for text in tal.texts {
if text.is_empty() {
continue
}
if annotations.length() >= 1000000 {
raise Limit("annotation count exceeds one million")
}
annotations.push({
record: r,
signal: s,
onset: time,
duration,
text,
})
}
}
}
if h.continuity != Plain && r == 0 && (onset < 0.0 || onset >= 1.0) {
raise Invalid(
"first record onset must be a nonnegative fraction of a second",
)
}
if r > 0 {
let expected = starts[r - 1] + h.duration
if onset < expected - 1.0e-7 {
raise Invalid("record times overlap or go backwards")
}
if h.continuity == Continuous && (onset - expected).abs() > 1.0e-7 {
raise Invalid("continuous marker contradicts timekeeping TALs")
}
}
starts.push(onset)
}
{ header: h, data, records, starts, annotations, }
}
///|
/// Preserve original header spelling, padding and TAL bytes; finalize unknown record count.
pub fn Recording::encode(self : Recording) -> Bytes raise EdfError {
let out = self.data.to_array()
put_text(out, 236, 8, self.records.to_string())
Bytes::from_array(out)
}
///|
pub fn Recording::digital(
self : Recording,
signal : Int,
record : Int,
sample : Int,
) -> Int raise EdfError {
let h = self.header
if signal < 0 ||
signal >= h.signals.length() ||
record < 0 ||
record >= self.records {
raise Invalid("signal or record index out of range")
}
let s = h.signals[signal]
if s.is_annotation() {
raise Invalid("annotation channel is not a numeric signal")
}
if sample < 0 || sample >= s.samples_per_record {
raise Invalid("sample index out of range")
}
read_sample(
self.data,
h.bytes +
record * h.record_bytes +
h.offsets[signal] +
sample * h.format.width(),
h.format.width(),
)
}
///|
pub fn Signal::to_physical(
self : Signal,
digital : Int,
) -> Double raise EdfError {
if self.is_annotation() {
raise Invalid("cannot scale annotations")
}
if self.digital_max <= self.digital_min ||
self.physical_max == self.physical_min {
raise Invalid("invalid calibration")
}
let v = (digital.to_double() - self.digital_min.to_double()) /
(self.digital_max.to_double() - self.digital_min.to_double()) *
(self.physical_max - self.physical_min) +
self.physical_min
if !finite(v) {
raise Limit("physical calibration overflow")
}
v
}
///|
/// Round to nearest digital step, rejecting out-of-range values instead of clipping.
pub fn Signal::to_digital(
self : Signal,
physical : Double,
) -> Int raise EdfError {
if !finite(physical) ||
self.is_annotation() ||
self.digital_max <= self.digital_min ||
self.physical_max == self.physical_min {
raise Invalid("invalid physical value or calibration")
}
let fraction = (physical - self.physical_min) /
(self.physical_max - self.physical_min)
if !finite(fraction) || fraction < 0.0 || fraction > 1.0 {
raise Invalid("physical value outside calibration range")
}
let value = fraction *
(self.digital_max.to_double() - self.digital_min.to_double()) +
self.digital_min.to_double()
value.round().to_int()
}
///|
pub fn Recording::physical(
self : Recording,
signal : Int,
record : Int,
sample : Int,
) -> Double raise EdfError {
let value = self.digital(signal, record, sample)
self.header.signals[signal].to_physical(value)
}
///|
pub fn Recording::sample_time(
self : Recording,
signal : Int,
record : Int,
sample : Int,
) -> Double raise EdfError {
ignore(self.digital(signal, record, sample))
self.starts[record] +
self.header.duration *
sample.to_double() /
self.header.signals[signal].samples_per_record.to_double()
}
///|
pub fn Recording::sample_rate(
self : Recording,
signal : Int,
) -> Double raise EdfError {
if signal < 0 ||
signal >= self.header.signals.length() ||
self.header.signals[signal].is_annotation() {
raise Invalid("ordinary signal index required")
}
if self.header.duration == 0.0 {
raise Unsupported("zero-duration event records have no sample rate")
}
self.header.signals[signal].samples_per_record.to_double() /
self.header.duration
}
///|
/// Construct EDF/BDF with ordinary channels and optional plus annotations.
pub fn create(
signals : Array[Signal],
records : Array[Record],
duration : Double,
format? : Format = Edf,
continuity? : Continuity = Continuous,
start? : DateTime = {
year: 2020,
month: 1,
day: 1,
hour: 0,
minute: 0,
second: 0,
},
patient? : String = "X X X X",
) -> Recording raise EdfError {
start.validate()
if records.length() > 1000000 {
raise Limit("record count exceeds limit")
}
for s in signals {
validate_signal(s, format)
if s.is_annotation() {
raise Invalid("create adds the annotation channel automatically")
}
}
let channels = signals.copy()
let blocks = []
let mut annotation_samples = 32
if continuity != Plain {
for r in records {
let tals : Array[Tal] = []
let first_texts = [""]
if signals.is_empty() {
if r.events.is_empty() ||
r.events[0].onset != r.onset ||
r.events[0].duration is Some(_) ||
r.events[0].text.is_empty() {
raise Invalid(
"annotation-only record requires a first untimed-duration event at record onset",
)
}
first_texts.push(r.events[0].text)
}
tals.push({
onset: time_text(r.onset, true),
duration: None,
texts: first_texts,
})
for event_index = (if signals.is_empty() { 1 } else { 0 })
event_index < r.events.length()
event_index = event_index + 1 {
let event = r.events[event_index]
tals.push({
onset: time_text(event.onset, true),
duration: match event.duration {
Some(d) => Some(time_text(d, false))
None => None
},
texts: [event.text],
})
}
// Determine byte capacity without a record-sized temporary allocation.
let mut bytes = 0
for tal in tals {
bytes += @utf8.encode(
tal.onset +
(match tal.duration {
Some(d) => "\u0015" + d
None => ""
}) +
"\u0014" +
tal.texts.join("\u0014") +
"\u0014\u0000",
).length()
if bytes > 8388608 {
raise Limit("annotations exceed record limit")
}
}
annotation_samples = annotation_samples.max(
(bytes + format.width() - 1) / format.width(),
)
blocks.push(tals)
}
channels.push({
label: if format == Edf {
"EDF Annotations"
} else {
"BDF Annotations"
},
transducer: "",
unit: "",
physical_min: -1.0,
physical_max: 1.0,
digital_min: if format == Edf {
-32768
} else {
-8388608
},
digital_max: if format == Edf {
32767
} else {
8388607
},
prefilter: "",
samples_per_record: annotation_samples,
reserved: "",
})
} else {
for i = 0; i < records.length(); i = i + 1 {
if !records[i].events.is_empty() ||
(records[i].onset - i.to_double() * duration).abs() > 1.0e-7 {
raise Invalid(
"plain recording cannot store events or discontinuous times",
)
}
}
}
let header_bytes = make_header(
format,
continuity,
patient,
"Startdate " + start.long_date() + " X X X",
start.date_field(),
start.time_field(),
"",
records.length(),
duration,
channels,
)
let h = parse_header(header_bytes)
if records.length() > (268435456 - h.bytes) / h.record_bytes {
raise Limit("output exceeds 256 MiB")
}
let out = Array::make(h.bytes + records.length() * h.record_bytes, b'\x00')
copy_bytes(out, 0, header_bytes, 0, header_bytes.length())
for r = 0; r < records.length(); r = r + 1 {
let record = records[r]
if record.samples.length() != signals.length() {
raise Invalid("record channel count mismatch")
}
for s = 0; s < signals.length(); s = s + 1 {
let values = record.samples[s]
let signal = signals[s]
if values.length() != signal.samples_per_record {
raise Invalid("record sample count mismatch")
}
for j = 0; j < values.length(); j = j + 1 {
let v = values[j]
if v < signal.digital_min || v > signal.digital_max {
raise Invalid("sample outside digital calibration limits")
}
put_sample(
out,
h.bytes + r * h.record_bytes + h.offsets[s] + j * format.width(),
format.width(),
v,
)
}
}
if continuity != Plain {
let bytes = encode_tals(blocks[r], annotation_samples * format.width())
copy_bytes(
out,
h.bytes + r * h.record_bytes + h.offsets[signals.length()],
bytes,
0,
bytes.length(),
)
}
}
decode(Bytes::from_array(out))
}