UNPKG

node-labstreaminglayer

Version:
278 lines 10.9 kB
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