@neurosity/sdk
Version:
Neurosity SDK
110 lines (109 loc) • 4.4 kB
JavaScript
import { pipe, from } from "rxjs";
import { mergeMap } from "rxjs/operators";
import { Buffer } from "buffer/index.js"; // not including /index.js causes typescript to uses Node's native Buffer built-in and we want to use this npm package for both node and the browser
import { epoch, addInfo } from "../../../utils/pipes";
const EPOCH_BUFFER_SIZE = 16;
const SAMPLING_RATE_FALLBACK = 256; // Crown's sampling rate
/** Size in bytes for each channel's payload. */
const TimestampSize = 8; // UInt64
const MarkerSize = 2; // UInt16
const ChannelDataSize = 8; // Double
/** Size in bytes for the static payload of every sample (Timestamp + Marker) */
const SampleFixedSize = TimestampSize + MarkerSize;
/**
* @hidden
*/
export function binaryBufferToEpoch(deviceInfo) {
var _a;
if (!(deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.samplingRate)) {
console.warn(`Didn't receive a sampling rate, defaulting to ${SAMPLING_RATE_FALLBACK}`);
}
return pipe(binaryBufferToSamples(deviceInfo.channels), epoch({
duration: EPOCH_BUFFER_SIZE,
interval: EPOCH_BUFFER_SIZE,
samplingRate: (_a = deviceInfo === null || deviceInfo === void 0 ? void 0 : deviceInfo.samplingRate) !== null && _a !== void 0 ? _a : SAMPLING_RATE_FALLBACK
}), addInfo({
channelNames: deviceInfo.channelNames,
samplingRate: deviceInfo.samplingRate
}));
}
/**
* @hidden
*/
export function binaryBufferToSamples(channelCount) {
return pipe(mergeMap((arrayBuffer) => {
const buffer = Buffer.from(arrayBuffer);
const decoded = decode(buffer, channelCount);
return from(decoded); // `from` creates an Observable emission from each item (Sample) in the array
}));
}
/**
* @hidden
*
* Decode the supplied Buffer as a list of Sample.
*
* Supplied buffer's length must be multiple of
* `encodedSampleSize(channelCount)`.
*
* NB: This method does not guarantee validity of decoded samples. When
* supplied with a buffer of appropriate length, it will always return a
* matching number of Sample8. Since the encoding protocol defines no
* metadata/checksum, correctness must be guaranteed via test coverage.
*
* @param buffer Buffer with binary payload to decode.
* @param channelCount Number of expected channels in each sample.
*
* @returns List of decoded Samples present in buffer.
*/
export function decode(buffer, channelCount) {
let sampleLen = encodedSampleSize(channelCount);
// Alternative: relax this check, process sampleLen at a time, discard remainder?
if (buffer.length % sampleLen != 0) {
throw new Error(`buffer.length (${buffer.length}) for ${channelCount} channels must be multiple of ${sampleLen}B)`);
}
let sampleCount = buffer.length / sampleLen;
let samples = new Array(sampleCount);
for (let i = 0; i < sampleCount; i++) {
let offset = i * sampleLen;
let channelData = new Array(channelCount);
// Read 8 bytes for timestamp & advance offset
let ts = buffer.readBigUInt64BE(offset);
offset += TimestampSize;
// Read 1 byte for marker & advance offset
let marker = buffer.readUInt16BE(offset);
offset += MarkerSize;
// Read 8 bytes for each channel & advance offset
for (let i = 0; i < channelCount; i++) {
channelData[i] = buffer.readDoubleBE(offset);
offset += ChannelDataSize;
}
samples[i] = {
timestamp: Number(ts),
// TODO: uncomment when ready
// marker: marker,
data: channelData
};
}
return samples;
}
/**
* @hidden
*
* Calculate the size of each sample based on the number of channels.
*
* Each sample has the following 3 segments:
* - Timestamp: 8 bytes (UInt64); contains current time in millis since epoch)
* - Marker: 2 bytes (UInt16); for classifier data
* - Data: N * 8 bytes (Double), each entry representing data from a different
* electrode.
*
* +-----------+--------+------------------+
* | timestamp | marker | data (e1 ... eN) |
* +-----------+--------+------------------+
*
* The number of entries for Data varies per hardware model. It can be assumed
* to remain constant for the lifetime of the program.
*/
export function encodedSampleSize(channelCount) {
return SampleFixedSize + channelCount * ChannelDataSize;
}