node-labstreaminglayer
Version:
Node.js bindings for Lab Streaming Layer (LSL)
278 lines • 10.9 kB
JavaScript
import { lsl_create_inlet, lsl_destroy_inlet, lsl_get_fullinfo, lsl_open_stream, lsl_close_stream, lsl_time_correction, lsl_set_postprocessing, lsl_samples_available, lsl_inlet_flush, lsl_was_clock_reset, fmt2pull_sample, fmt2pull_chunk, cf_string } from './lib/index.js';
import { StreamInfo } from './streamInfo.js';
import { handleError, FOREVER, InvalidArgumentError } from './util.js';
// FinalizationRegistry for automatic cleanup
const inletRegistry = new FinalizationRegistry((obj) => {
try {
lsl_destroy_inlet(obj);
}
catch (e) {
// Silently ignore cleanup errors
}
});
/**
* A stream inlet receives streaming data from the network.
* Inlets are used to receive data from a specific stream.
*/
export class StreamInlet {
obj; // Pointer to the LSL inlet object
channelFormat;
channelCount;
doPullSample;
doPullChunk;
sampleBuffer; // Reusable buffer for pulling samples
chunkBuffers; // Reusable buffers for pulling chunks
constructor(info, maxBuflen = 360, maxChunklen = 0, recover = true, processingFlags = 0) {
// Validate info parameter
if (Array.isArray(info)) {
throw new TypeError('Description needs to be of type StreamInfo, got a list.');
}
// Create the inlet
this.obj = lsl_create_inlet(info.getHandle(), maxBuflen, maxChunklen, recover ? 1 : 0);
if (!this.obj) {
throw new Error('Could not create stream inlet.');
}
// Register for automatic cleanup
inletRegistry.register(this, this.obj, this);
// Set post-processing flags if specified
if (processingFlags > 0) {
const result = lsl_set_postprocessing(this.obj, processingFlags);
handleError(result);
}
// Store stream properties for efficient pulling
this.channelFormat = info.channelFormat();
this.channelCount = info.channelCount();
// Get the appropriate pull functions for this data type
this.doPullSample = fmt2pull_sample[this.channelFormat];
this.doPullChunk = fmt2pull_chunk[this.channelFormat];
if (!this.doPullSample) {
throw new Error(`Unsupported channel format: ${this.channelFormat}`);
}
// Create reusable sample buffer
this._createSampleBuffer();
// Initialize chunk buffer cache
this.chunkBuffers = new Map();
}
_createSampleBuffer() {
if (this.channelFormat === cf_string) {
// For strings, we need an array of string pointers
this.sampleBuffer = new Array(this.channelCount).fill('');
}
else {
// For numeric types, create appropriate typed array
let TypedArray;
switch (this.channelFormat) {
case 1: // cf_float32
TypedArray = Float32Array;
break;
case 2: // cf_double64
TypedArray = Float64Array;
break;
case 4: // cf_int32
TypedArray = Int32Array;
break;
case 5: // cf_int16
TypedArray = Int16Array;
break;
case 6: // cf_int8
TypedArray = Int8Array;
break;
case 7: // cf_int64
TypedArray = BigInt64Array;
break;
default:
throw new Error(`Unsupported channel format: ${this.channelFormat}`);
}
this.sampleBuffer = new TypedArray(this.channelCount);
}
}
/**
* Destroy the inlet and free resources.
* Called automatically when the object is garbage collected.
*/
destroy() {
if (this.obj) {
try {
inletRegistry.unregister(this);
lsl_destroy_inlet(this.obj);
}
catch (e) {
// Silently ignore errors during destruction
}
this.obj = null;
}
}
info(timeout = FOREVER) {
const errcode = [0];
const result = lsl_get_fullinfo(this.obj, timeout, errcode);
handleError(errcode[0]);
return new StreamInfo('', '', 0, 0, 0, '', result);
}
openStream(timeout = FOREVER) {
const errcode = [0];
lsl_open_stream(this.obj, timeout, errcode);
handleError(errcode[0]);
}
closeStream() {
lsl_close_stream(this.obj);
}
timeCorrection(timeout = FOREVER) {
const errcode = [0];
const result = lsl_time_correction(this.obj, timeout, errcode);
handleError(errcode[0]);
return result;
}
/**
* Pull a single sample from the inlet.
* @param timeout Timeout in seconds (default: FOREVER)
* @param sample Optional array to fill with sample data
* @returns Tuple of [sample, timestamp] or [null, null] if no sample available
*/
pullSample(timeout = FOREVER, sample) {
// Input validation
if (typeof timeout !== 'number' || timeout < 0) {
throw new InvalidArgumentError('Timeout must be a non-negative number');
}
const errcode = [0];
// Pull the sample
const timestamp = this.doPullSample(this.obj, this.sampleBuffer, this.channelCount, timeout, errcode);
handleError(errcode[0]);
if (timestamp) {
// Convert buffer to JavaScript array
let sampleArray;
if (this.channelFormat === cf_string) {
// For strings, decode from buffer
sampleArray = [];
for (let i = 0; i < this.channelCount; i++) {
sampleArray.push(this.sampleBuffer[i]);
}
}
else {
// For numeric types, convert from typed array
sampleArray = Array.from(this.sampleBuffer);
}
// If sample parameter was provided (legacy API), copy to it
if (sample && Array.isArray(sample)) {
sample.length = 0;
sample.push(...sampleArray);
}
return [sampleArray, timestamp];
}
else {
return [null, null];
}
}
/**
* Pull a chunk of samples from the inlet.
* @param timeout Timeout in seconds (default: 0)
* @param maxSamples Maximum number of samples to pull
* @param destObj Optional destination buffer
* @returns Tuple of [samples, timestamps]
*/
pullChunk(timeout = 0.0, maxSamples = 1024, destObj) {
// Input validation
if (typeof timeout !== 'number' || timeout < 0) {
throw new InvalidArgumentError('Timeout must be a non-negative number');
}
if (typeof maxSamples !== 'number' || maxSamples <= 0) {
throw new InvalidArgumentError('maxSamples must be a positive number');
}
// Get or create reusable buffers for this size
const maxValues = maxSamples * this.channelCount;
if (!this.chunkBuffers.has(maxSamples)) {
// Create new buffers for this size
let dataBuffer;
if (this.channelFormat === cf_string) {
// For strings, array of string pointers
dataBuffer = new Array(maxValues).fill('');
}
else {
// For numeric types
let TypedArray;
switch (this.channelFormat) {
case 1: // cf_float32
TypedArray = Float32Array;
break;
case 2: // cf_double64
TypedArray = Float64Array;
break;
case 4: // cf_int32
TypedArray = Int32Array;
break;
case 5: // cf_int16
TypedArray = Int16Array;
break;
case 6: // cf_int8
TypedArray = Int8Array;
break;
case 7: // cf_int64
TypedArray = BigInt64Array;
break;
default:
throw new Error(`Unsupported channel format: ${this.channelFormat}`);
}
dataBuffer = destObj ? new TypedArray(destObj) : new TypedArray(maxValues);
}
const timestampBuffer = new Float64Array(maxSamples);
this.chunkBuffers.set(maxSamples, {
data: dataBuffer,
timestamps: timestampBuffer
});
}
const buffers = this.chunkBuffers.get(maxSamples);
const dataBuffer = destObj || buffers.data;
const timestampBuffer = buffers.timestamps;
// Pull the chunk
const errcode = [0];
const numElements = this.doPullChunk(this.obj, dataBuffer, timestampBuffer, maxValues, maxSamples, timeout, errcode);
handleError(errcode[0]);
const numSamplesPulled = Math.floor(numElements / this.channelCount);
// Convert to output format
let samples = null;
if (destObj === undefined && numSamplesPulled > 0) {
samples = [];
if (this.channelFormat === cf_string) {
// For strings
for (let s = 0; s < numSamplesPulled; s++) {
const sample = [];
for (let c = 0; c < this.channelCount; c++) {
sample.push(dataBuffer[s * this.channelCount + c]);
}
samples.push(sample);
}
// Free string memory if needed
for (let i = 0; i < numElements; i++) {
if (dataBuffer[i]) {
// Note: Koffi should handle string memory automatically
}
}
}
else {
// For numeric types
for (let s = 0; s < numSamplesPulled; s++) {
const sample = [];
for (let c = 0; c < this.channelCount; c++) {
sample.push(dataBuffer[s * this.channelCount + c]);
}
samples.push(sample);
}
}
}
// Extract timestamps
const timestamps = [];
for (let i = 0; i < numSamplesPulled; i++) {
timestamps.push(timestampBuffer[i]);
}
return [samples, timestamps];
}
samplesAvailable() {
return lsl_samples_available(this.obj);
}
flush() {
return lsl_inlet_flush(this.obj);
}
wasClockReset() {
return Boolean(lsl_was_clock_reset(this.obj));
}
}
//# sourceMappingURL=inlet.js.map