wfdb
Version:
Library for accessing WaveForm DataBase files.
220 lines (193 loc) • 8.55 kB
JavaScript
"use strict";
var LineTransform = require('./wfdb-util').LineTransform;
var stream = require('stream');
var util = require('util');
var path = require('path');
function HeaderTransform(opts) {
// allow use without new
if (!(this instanceof HeaderTransform)) {
return new HeaderTransform(opts);
}
opts = opts || {};
opts.readableObjectMode = true;
opts.objectMode = true; // for backward compatibility
stream.Transform.call(this, opts);
this.record = opts.record;
this.wfdb = opts.wfdb;
this.segmentIndex = 0;
};
util.inherits(HeaderTransform, stream.Transform);
var COMMENTS = /^\s*#.*\s*$/;
var BLANK = /^\s*$/;
var HEADER = /^(\S+)(?:\/(\d+))?\s+(\d+)\s+([0-9e\-.]+)?(?:\/([0-9e\-.]+))?(?:\(([\d-.]+)\))?(?:\s+(\d+))?(?:\s+(\S+))?(?:\s+(\S+))?/;
var LAYOUT = /^.+_layout$/;
var SIGNAL = /^(\S+)\s+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?(?:[Ee]\d+)?)(?:x([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\:([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\s+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\(([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?)\))?(?:\/(\S+))?(?:\s+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\s+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\s+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\s+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\s+([+-]?\d*(?:\.\d+)?(?:[Ee]\d+)?))?(?:\s+(.+))?\r?$/;
var SEGMENT = /^(\S+)\s+(\d+)/;
HeaderTransform.prototype.readHeader = function(data) {
var header = HEADER.exec(data);
if(!header) {
return new Error("Cannot parse header descriptor: " + data);
} else {
this.header = {
"record": this.record,
name: header[1],
number_of_segments: header[2]?+header[2]:header[2],
number_of_signals: +header[3] || 0,
sampling_frequency: +header[4] || 250,
counter_frequency: +header[5] || 0,
base_counter_value: +header[6] || 0,
number_of_samples_per_signal: +header[7],
base_time: header[8] || '0:0:0',
base_date: header[9],
signals: [],
segments: [],
total_bits_per_frame: 0,
highest_adc_resolution: 0,
highest_samples_per_frame: 0
};
}
};
HeaderTransform.prototype.readSignal = function(data) {
var arr = SIGNAL.exec(data);
if(!arr) {
console.log("Cannot parse signal descriptor: " + data);
return new Error("Cannot parse signal descriptor: " + data);
} else {
var signal =
{
file_name: arr[1],
format: arr[2] | 0,
samples_per_frame: (arr[3] | 0) || 1,
skew: arr[4] || 0,
byte_offset: arr[5] || 0,
adc_gain: parseFloat(arr[6]) || this.wfdb.DEFGAIN,
baseline: parseFloat(arr[7]) || parseFloat(arr[10]) || 0,
units: arr[8] || "",
adc_resolution: (arr[9] | 0) || 12, // covers only amplitude formats
adc_zero: parseFloat(arr[10]) || 0,
initial_value: arr[11] || arr[6] || 200,
checksum: arr[12] || 0,
block_size: arr[13] || 0,
description: arr[14] || "",
index: this.header.signals.length
};
signal.mask = 0;
for(var j = 0; j < signal.adc_resolution; j++) {
signal.mask |= 1 << j;
}
this.header.signals.push(signal);
if(signal.skew) {
this.header.max_skew = Math.max(this.header.max_skew || 0, signal.skew);
}
if(undefined===this.wfdb.accepted_formats[signal.format]) {
var discarded = this.header.signals.pop();
console.log("Cannot decode format " + discarded.format + " for " + discarded.description);
} else {
signal.bits_per_frame = this.wfdb.accepted_formats[signal.format] * signal.samples_per_frame;
this.header.total_bits_per_frame += signal.bits_per_frame;
if(signal.adc_resolution>this.header.highest_adc_resolution) {
this.header.highest_adc_resolution = signal.adc_resolution;
}
if(signal.samples_per_frame>this.header.highest_samples_per_frame) {
this.header.highest_samples_per_frame = signal.samples_per_frame;
}
}
}
};
HeaderTransform.prototype.readSegment = function(data, next) {
var base = this.header.record.split("/");
base.pop();
base = base.join("/");
var arr = SEGMENT.exec(data);
if(!arr) {
next(new Error('Cannot parse segment descriptor: ' + data));
} else {
var segment =
{
name: arr[1],
number_of_samples_per_signal: +arr[2]
};
var self = this;
if(!segment.name.match(LAYOUT)) {
this.header.segments.push(segment);
}
if(segment.name != '~') {
readHeader(this.wfdb, base+'/'+segment.name)
.on('error',
function(err) { console.error(err, segment.name); next(err); })
.on('end',
function() { self.segmentIndex += segment.number_of_samples_per_signal; next(); })
.on('data', function(header) {
segment.header = header;
if(segment.header.number_of_samples_per_signal>0) {
segment.header.start = self.segmentIndex;
segment.header.end = self.segmentIndex + segment.number_of_samples_per_signal;
}
// TODO this is a little hacky for now importing layout signals to the top level
if(segment.name.match(LAYOUT)) {
self.header.signals = segment.header.signals;
self.header.signalIndex = {};
for(var i = 0; i < self.header.signals.length; i++) {
self.header.signalIndex[self.header.signals[i].description] = i;
}
} else {
// This is also hacky to allow signals to occupy the same position in data arrays regardless of their position in the segment
// Further we are assuming that the "layout" file is the first segment to be processed
for(var i = 0; i < segment.header.signals.length; i++) {
segment.header.signals[i].index = self.header.signalIndex[segment.header.signals[i].description];
}
}
});
} else {
self.segmentIndex += segment.number_of_samples_per_signal;
next();
}
}
};
HeaderTransform.prototype._transform = function(chunk, enc, next) {
var data = chunk.toString('ascii');
// Comment lines
if(data.match(COMMENTS)) {
next();
// Blank lines
} else if(data.match(BLANK)) {
next();
} else if(!this.header) {
next(this.readHeader(data));
} else if(this.header.number_of_segments) {
this.readSegment(data, next);
} else {
next(this.readSignal(data));
}
};
HeaderTransform.prototype._flush = function(next) {
if(!this.header.number_of_segments) {
// If not segments then calculate total_bytes_per_frame at the top level
this.header.total_bytes_per_frame = this.header.total_bits_per_frame / 8;
var file_name = null;
for(var i = 0; i < this.header.signals.length; i++) {
if(null == file_name) {
file_name = this.header.signals[i].file_name;
} else {
if(file_name != this.header.signals[i].file_name) {
this.emit('error', "cannot support mixed data file names");
}
}
}
this.header.file_name = path.dirname(this.header.record) + '/' + file_name;
}
this.push(this.header);
this.header = null;
next();
};
// Use this in your own pipelines with your own Readable
exports.HeaderTransform = HeaderTransform;
function readHeader(wfdb, record) {
var pipe =
wfdb.locator.locate(record+'.hea',{highWaterMark:wfdb.highWaterMark}).on('error', function(err) { pipe.emit('error', err); })
.pipe(LineTransform()).on('error', function(err) { pipe.emit('error', err); })
.pipe(HeaderTransform({'wfdb': wfdb, 'record':record}));
return pipe;
}
// use this to assemble a pipeline from a locator
module.exports = exports = readHeader;