diff --git a/pylsl/__init__.py b/pylsl/__init__.py index 36b86e1..1b76910 100644 --- a/pylsl/__init__.py +++ b/pylsl/__init__.py @@ -3,6 +3,8 @@ DEDUCED_TIMESTAMP, FOREVER, IRREGULAR_RATE, + ChannelValueFormats, + PostProcessingFlags, ContinuousResolver, InternalError, InvalidArgumentError, diff --git a/pylsl/examples/GetTimeCorrection.py b/pylsl/examples/GetTimeCorrection.py index 7602124..76f4fdf 100644 --- a/pylsl/examples/GetTimeCorrection.py +++ b/pylsl/examples/GetTimeCorrection.py @@ -2,13 +2,13 @@ import time -from pylsl import StreamInlet, resolve_stream +from pylsl import StreamInlet, resolve_byprop def main(): # first resolve an EEG stream on the lab network print("looking for an EEG stream...") - streams = resolve_stream("type", "EEG") + streams = resolve_byprop("type", "EEG") info = streams[0] # create a new inlet to read from the stream diff --git a/pylsl/examples/HandleMetadata.py b/pylsl/examples/HandleMetadata.py index f31308c..99d6f50 100644 --- a/pylsl/examples/HandleMetadata.py +++ b/pylsl/examples/HandleMetadata.py @@ -3,12 +3,12 @@ import time -from pylsl import StreamInfo, StreamInlet, StreamOutlet, resolve_stream +from pylsl import StreamInfo, StreamInlet, StreamOutlet, ChannelValueFormats, resolve_byprop def main(): # create a new StreamInfo object which shall describe our stream - info = StreamInfo("MetaTester", "EEG", 8, 100, "float32", "myuid56872") + info = StreamInfo("MetaTester", "EEG", 8, 100, ChannelValueFormats.FLOAT32.value, "myuid56872") # now attach some meta-data (in accordance with XDF format, # see also https://github.com/sccn/xdf/wiki/Meta-Data) @@ -33,7 +33,7 @@ def main(): # first we resolve a stream whose name is MetaTester (note that there are # other ways to query a stream, too - for instance by content-type) - results = resolve_stream("name", "MetaTester") + results = resolve_byprop("name", "MetaTester") # open an inlet so we can read the stream's data (and meta-data) inlet = StreamInlet(results[0]) diff --git a/pylsl/examples/PerformanceTest.py b/pylsl/examples/PerformanceTest.py index 186f038..6429d57 100644 --- a/pylsl/examples/PerformanceTest.py +++ b/pylsl/examples/PerformanceTest.py @@ -3,17 +3,8 @@ import numpy as np -from pylsl import ( - StreamInfo, - StreamInlet, - StreamOutlet, - local_clock, - proc_clocksync, - proc_dejitter, - proc_monotonize, - resolve_bypred, - resolve_byprop, -) +from pylsl import StreamInfo, StreamOutlet, local_clock, resolve_byprop, PostProcessingFlags, StreamInlet, \ + resolve_bypred try: from pyfftw.interfaces.numpy_fft import ( # Performs much better than numpy's fftpack @@ -102,13 +93,13 @@ def generate(self): class BetaGeneratorOutlet(object): def __init__( - self, - Fs=2**14, - FreqBeta=20.0, - AmpBeta=100.0, - AmpNoise=20.0, - NCyclesPerChunk=4, - channels=["RAW1", "SPK1", "RAW2", "SPK2", "RAW3", "SPK3"], + self, + Fs=2 ** 14, + FreqBeta=20.0, + AmpBeta=100.0, + AmpNoise=20.0, + NCyclesPerChunk=4, + channels=["RAW1", "SPK1", "RAW2", "SPK2", "RAW3", "SPK3"], ): """ :param Fs: Sampling rate @@ -189,7 +180,7 @@ def __init__(self): streams = resolve_byprop("type", "EEG") # create a new inlet to read from the stream - proc_flags = proc_clocksync | proc_dejitter | proc_monotonize + proc_flags = PostProcessingFlags.CLOCKSYNC.value | PostProcessingFlags.DEJITTER.value | PostProcessingFlags.MONOTONIZE.value self.inlet = StreamInlet(streams[0], processing_flags=proc_flags) # The following is an example of how to read stream info @@ -227,11 +218,11 @@ class MarkersGeneratorOutlet(object): } def __init__( - self, - class_list=[1, 3], - classes_rand=True, - target_list=[1, 2], - targets_rand=True, + self, + class_list=[1, 3], + classes_rand=True, + target_list=[1, 2], + targets_rand=True, ): """ @@ -289,7 +280,7 @@ def update(self): else self.target_list[ (self.target_list.index(self.target_id) + 1) % len(self.target_list) - ] + ] ) self.class_id = ( random.choice(self.class_list) @@ -297,7 +288,7 @@ def update(self): else self.class_list[ (self.class_list.index(self.class_id) + 1) % len(self.class_list) - ] + ] ) # print("New class_id: {}, target_id: {}".format(self.class_id, self.target_id)) out_string = "NewTrial {}, Class {}, Target {}".format( diff --git a/pylsl/examples/ReceiveAndPlot.py b/pylsl/examples/ReceiveAndPlot.py index 70f0154..3c69d2c 100644 --- a/pylsl/examples/ReceiveAndPlot.py +++ b/pylsl/examples/ReceiveAndPlot.py @@ -16,7 +16,8 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui -import pylsl +from pylsl import StreamInfo, StreamInlet, PostProcessingFlags, resolve_streams, IRREGULAR_RATE, ChannelValueFormats, \ + local_clock # Basic parameters for the plotting window plot_duration = 5 # how many seconds of data to show @@ -27,7 +28,7 @@ class Inlet: """Base class to represent a plottable inlet""" - def __init__(self, info: pylsl.StreamInfo): + def __init__(self, info: StreamInfo): # create an inlet and connect it to the outlet we found earlier. # max_buflen is set so data older the plot_duration is discarded # automatically and we only pull data new enough to show it @@ -36,10 +37,10 @@ def __init__(self, info: pylsl.StreamInfo): # same time domain as the local lsl_clock() # (see https://labstreaminglayer.readthedocs.io/projects/liblsl/ref/enums.html#_CPPv414proc_clocksync) # and dejitter timestamps - self.inlet = pylsl.StreamInlet( + self.inlet = StreamInlet( info, max_buflen=plot_duration, - processing_flags=pylsl.proc_clocksync | pylsl.proc_dejitter, + processing_flags=PostProcessingFlags.CLOCKSYNC.value | PostProcessingFlags.DEJITTER.value, ) # store the name and channel count self.name = info.name() @@ -60,7 +61,7 @@ class DataInlet(Inlet): dtypes = [[], np.float32, np.float64, None, np.int32, np.int16, np.int8, np.int64] - def __init__(self, info: pylsl.StreamInfo, plt: pg.PlotItem): + def __init__(self, info: StreamInfo, plt: pg.PlotItem): super().__init__(info) # calculate the size for our buffer, i.e. two times the displayed data bufsize = ( @@ -85,7 +86,7 @@ def pull_and_plot(self, plot_time, plt): # ts will be empty if no samples were pulled, a list of timestamps otherwise if ts: ts = np.asarray(ts) - y = self.buffer[0 : ts.size, :] + y = self.buffer[0: ts.size, :] this_x = None old_offset = 0 new_offset = 0 @@ -113,7 +114,7 @@ def pull_and_plot(self, plot_time, plt): class MarkerInlet(Inlet): """A MarkerInlet shows events that happen sporadically as vertical lines""" - def __init__(self, info: pylsl.StreamInfo): + def __init__(self, info: StreamInfo): super().__init__(info) def pull_and_plot(self, plot_time, plt): @@ -130,7 +131,7 @@ def main(): # firstly resolve all streams that could be shown inlets: List[Inlet] = [] print("looking for streams") - streams = pylsl.resolve_streams() + streams = resolve_streams() # Create the pyqtgraph window pw = pg.plot(title="LSL Plot") @@ -142,15 +143,15 @@ def main(): for info in streams: if info.type() == "Markers": if ( - info.nominal_srate() != pylsl.IRREGULAR_RATE - or info.channel_format() != pylsl.cf_string + info.nominal_srate() != IRREGULAR_RATE + or info.channel_format() != ChannelValueFormats.STRING.value ): print("Invalid marker stream " + info.name()) print("Adding marker inlet: " + info.name()) inlets.append(MarkerInlet(info)) elif ( - info.nominal_srate() != pylsl.IRREGULAR_RATE - and info.channel_format() != pylsl.cf_string + info.nominal_srate() != IRREGULAR_RATE + and info.channel_format() != ChannelValueFormats.STRING.value ): print("Adding data inlet: " + info.name()) inlets.append(DataInlet(info, plt)) @@ -162,12 +163,12 @@ def scroll(): # We show data only up to a timepoint shortly before the current time # so new data doesn't suddenly appear in the middle of the plot fudge_factor = pull_interval * 0.002 - plot_time = pylsl.local_clock() + plot_time = local_clock() pw.setXRange(plot_time - plot_duration + fudge_factor, plot_time - fudge_factor) def update(): # Read data from the inlet. Use a timeout of 0.0 so we don't block GUI interaction. - mintime = pylsl.local_clock() - plot_duration + mintime = local_clock() - plot_duration # call pull_and_plot for each inlet. # Special handling of inlet types (markers, continuous data) is done in # the different inlet classes. diff --git a/pylsl/examples/ReceiveData.py b/pylsl/examples/ReceiveData.py index 04406f8..8615d21 100644 --- a/pylsl/examples/ReceiveData.py +++ b/pylsl/examples/ReceiveData.py @@ -1,12 +1,12 @@ """Example program to show how to read a multi-channel time series from LSL.""" -from pylsl import StreamInlet, resolve_stream +from pylsl import StreamInlet, resolve_byprop def main(): # first resolve an EEG stream on the lab network print("looking for an EEG stream...") - streams = resolve_stream("type", "EEG") + streams = resolve_byprop("type", "EEG") # create a new inlet to read from the stream inlet = StreamInlet(streams[0]) diff --git a/pylsl/examples/ReceiveDataInChunks.py b/pylsl/examples/ReceiveDataInChunks.py index e91e0bd..817846b 100644 --- a/pylsl/examples/ReceiveDataInChunks.py +++ b/pylsl/examples/ReceiveDataInChunks.py @@ -1,13 +1,13 @@ """Example program to demonstrate how to read a multi-channel time-series from LSL in a chunk-by-chunk manner (which is more efficient).""" -from pylsl import StreamInlet, resolve_stream +from pylsl import StreamInlet, resolve_byprop def main(): # first resolve an EEG stream on the lab network print("looking for an EEG stream...") - streams = resolve_stream("type", "EEG") + streams = resolve_byprop("type", "EEG") # create a new inlet to read from the stream inlet = StreamInlet(streams[0]) diff --git a/pylsl/examples/ReceiveStringMarkers.py b/pylsl/examples/ReceiveStringMarkers.py index f8dacec..78cc413 100644 --- a/pylsl/examples/ReceiveStringMarkers.py +++ b/pylsl/examples/ReceiveStringMarkers.py @@ -1,12 +1,12 @@ """Example program to demonstrate how to read string-valued markers from LSL.""" -from pylsl import StreamInlet, resolve_stream +from pylsl import StreamInlet, resolve_byprop def main(): # first resolve a marker stream on the lab network print("looking for a marker stream...") - streams = resolve_stream("type", "Markers") + streams = resolve_byprop("type", "Markers") # create a new inlet to read from the stream inlet = StreamInlet(streams[0]) diff --git a/pylsl/examples/SendData.py b/pylsl/examples/SendData.py index 7e89a06..8bd8e70 100644 --- a/pylsl/examples/SendData.py +++ b/pylsl/examples/SendData.py @@ -5,7 +5,7 @@ import time from random import random as rand -from pylsl import StreamInfo, StreamOutlet, local_clock +from pylsl import StreamInfo, StreamOutlet, local_clock, ChannelValueFormats def main(argv): @@ -39,7 +39,7 @@ def main(argv): # last value would be the serial number of the device or some other more or # less locally unique identifier for the stream as far as available (you # could also omit it but interrupted connections wouldn't auto-recover) - info = StreamInfo(name, type, n_channels, srate, "float32", "myuid34234") + info = StreamInfo(name, type, n_channels, srate, ChannelValueFormats.FLOAT32.value, "myuid34234") # next make an outlet outlet = StreamOutlet(info) diff --git a/pylsl/examples/SendDataAdvanced.py b/pylsl/examples/SendDataAdvanced.py index d7b8395..e011ddc 100644 --- a/pylsl/examples/SendDataAdvanced.py +++ b/pylsl/examples/SendDataAdvanced.py @@ -4,7 +4,7 @@ import time from random import random as rand -import pylsl +from pylsl import ChannelValueFormats, StreamInfo, StreamOutlet, local_clock def main(name="LSLExampleAmp", stream_type="EEG", srate=100): @@ -35,8 +35,8 @@ def main(name="LSLExampleAmp", stream_type="EEG", srate=100): # The last value would be the serial number of the device or some other more or # less locally unique identifier for the stream as far as available (you # could also omit it but interrupted connections wouldn't auto-recover). - info = pylsl.StreamInfo( - name, stream_type, n_channels, srate, "float32", "myuid2424" + info = StreamInfo( + name, stream_type, n_channels, srate, ChannelValueFormats.FLOAT32.value, "myuid2424" ) # append some meta-data @@ -59,7 +59,7 @@ def main(name="LSLExampleAmp", stream_type="EEG", srate=100): # next make an outlet; we set the transmission chunk size to 32 samples # and the outgoing buffer size to 360 seconds (max.) - outlet = pylsl.StreamOutlet(info, 32, 360) + outlet = StreamOutlet(info, 32, 360) if False: # It's unnecessary to check the info when the stream was created in the same scope; just use info. @@ -68,14 +68,14 @@ def main(name="LSLExampleAmp", stream_type="EEG", srate=100): assert check_info.name() == name assert check_info.type() == stream_type assert check_info.channel_count() == len(channel_names) - assert check_info.channel_format() == pylsl.cf_float32 + assert check_info.channel_format() == ChannelValueFormats.FLOAT32.value assert check_info.nominal_srate() == srate print("now sending data...") - start_time = pylsl.local_clock() + start_time = local_clock() sent_samples = 0 while True: - elapsed_time = pylsl.local_clock() - start_time + elapsed_time = local_clock() - start_time required_samples = int(srate * elapsed_time) - sent_samples if required_samples > 0: # make a chunk==array of length required_samples, where each element in the array @@ -86,7 +86,7 @@ def main(name="LSLExampleAmp", stream_type="EEG", srate=100): ] # Get a time stamp in seconds. We pretend that our samples are actually # 125ms old, e.g., as if coming from some external hardware with known latency. - stamp = pylsl.local_clock() - 0.125 + stamp = local_clock() - 0.125 # now send it and wait for a bit # Note that even though `rand()` returns a 64-bit value, the `push_chunk` method # will convert it to c_float before passing the data to liblsl. diff --git a/pylsl/examples/SendStringMarkers.py b/pylsl/examples/SendStringMarkers.py index 21eeab8..fdf0ee1 100644 --- a/pylsl/examples/SendStringMarkers.py +++ b/pylsl/examples/SendStringMarkers.py @@ -3,7 +3,7 @@ import random import time -from pylsl import StreamInfo, StreamOutlet +from pylsl import StreamInfo, StreamOutlet, ChannelValueFormats def main(): @@ -15,7 +15,7 @@ def main(): # connections wouldn't auto-recover). The important part is that the # content-type is set to 'Markers', because then other programs will know how # to interpret the content - info = StreamInfo("MyMarkerStream", "Markers", 1, 0, "string", "myuidw43536") + info = StreamInfo("MyMarkerStream", "Markers", 1, 0, ChannelValueFormats.STRING.value, "myuidw43536") # next make an outlet outlet = StreamOutlet(info) diff --git a/pylsl/pylsl.py b/pylsl/pylsl.py index 1726716..97c1c03 100644 --- a/pylsl/pylsl.py +++ b/pylsl/pylsl.py @@ -36,11 +36,14 @@ cast, util, ) +from enum import IntEnum __all__ = [ "IRREGULAR_RATE", "DEDUCED_TIMESTAMP", "FOREVER", + "ChannelValueFormats", + "PostProcessingFlags", "cf_float32", "cf_double64", "cf_string", @@ -96,43 +99,93 @@ # A very large time value (ca. 1 year); can be used in timeouts. FOREVER = 32000000.0 -# Value formats supported by LSL. LSL data streams are sequences of samples, -# each of which is a same-size vector of values with one of the below types. -# For up to 24-bit precision measurements in the appropriate physical unit ( -# e.g., microvolts). Integers from -16777216 to 16777216 are represented -# accurately. -cf_float32 = 1 -# For universal numeric data as long as permitted by network and disk budget. -# The largest representable integer is 53-bit. -cf_double64 = 2 -# For variable-length ASCII strings or data blobs, such as video frames, -# complex event descriptions, etc. -cf_string = 3 -# For high-rate digitized formats that require 32-bit precision. Depends -# critically on meta-data to represent meaningful units. Useful for -# application event codes or other coded data. -cf_int32 = 4 -# For very high bandwidth signals or CD quality audio (for professional audio -# float is recommended). -cf_int16 = 5 -# For binary signals or other coded data. -cf_int8 = 6 -# For now only for future compatibility. Support for this type is not -# available on all languages and platforms. -cf_int64 = 7 -# Can not be transmitted. -cf_undefined = 0 +class ChannelValueFormats(IntEnum): + """ + An enum class for the value formats supported by LSL. + LSL data streams are sequences of samples, each of which is a same-size vector of the below types. + """ -# Post processing flags -proc_none = 0 # No automatic post-processing; return the ground-truth time stamps for manual post-processing. -proc_clocksync = 1 # Perform automatic clock synchronization; equivalent to manually adding the time_correction(). -proc_dejitter = 2 # Remove jitter from time stamps using a smoothing algorithm to the received time stamps. -proc_monotonize = 4 # Force the time-stamps to be monotonically ascending. Only makes sense if timestamps are dejittered. -proc_threadsafe = 8 # Post-processing is thread-safe (same inlet can be read from by multiple threads). -proc_ALL = ( - proc_none | proc_clocksync | proc_dejitter | proc_monotonize | proc_threadsafe -) + UNDEFINED = 0 + """ + Cannot be transmitted. + """ + + FLOAT32 = 1 + """ + For up to 24-bit precision measurements in the appropriate physical unit (e.g. microvolts). + Integers from -16777216 to 16777216 are represented accurately. + """ + + DOUBLE64 = 2 + """ + For universal numeric data as long as permitted by network and disk budget. + The largest representable integer is 53-bit. + """ + + STRING = 3 + """ + For variable-length ASCII strings or data blobs, such as video frames, complex event descriptions, etc. + """ + + INT32 = 4 + """ + For high-rate digitized formats that require 32-bit precision. + Depends critically on meta-data to represent meaningful units. + Useful for application event codes or other coded data. + """ + + INT16 = 5 + """ + For very high bandwidth signals or CD quality audio (for professional audio float is recommended). + """ + + INT8 = 6 + """ + For binary signals or other coded data. + """ + + INT64 = 7 + """ + For now only for future compatibility. + Support for this type is not available on all languages and platforms. + """ + + +class PostProcessingFlags(IntEnum): + """ + An enum class for the post-processing flags supported by LSL. + """ + + NONE = 0 + """ + No automatic post-processing; return the ground-truth time stamps for manual post-processing. + """ + + CLOCKSYNC = 1 + """ + Perform automatic clock synchronization; equivalent to manually adding the time_correction(). + """ + + DEJITTER = 2 + """ + Remove jitter from time stamps using a smoothing algorithm to the received time stamps. + """ + + MONOTONIZE = 4 + """ + Force the time-stamps to be monotonically ascending. Only makes sense if timestamps are dejittered. + """ + + THREADSAFE = 8 + """ + Post-processing is thread-safe (same inlet can be read from by multiple threads). + """ + + ALL = NONE | THREADSAFE | DEJITTER | MONOTONIZE | THREADSAFE + """ + All post-processing flags + """ # ========================================================== @@ -210,14 +263,14 @@ class StreamInfo: """ def __init__( - self, - name="untitled", - type="", - channel_count=1, - nominal_srate=IRREGULAR_RATE, - channel_format=cf_float32, - source_id="", - handle=None, + self, + name="untitled", + type="", + channel_count=1, + nominal_srate=IRREGULAR_RATE, + channel_format=ChannelValueFormats.FLOAT32.value, + source_id="", + handle=None, ): """Construct a new StreamInfo object. @@ -680,7 +733,7 @@ def push_sample(self, x, timestamp=0.0, pushthrough=True): """ if len(x) == self.channel_count: - if self.channel_format == cf_string: + if self.channel_format == ChannelValueFormats.STRING.value: x = [v.encode("utf-8") for v in x] handle_error( self.do_push_sample( @@ -750,7 +803,7 @@ def push_chunk(self, x, timestamp=0.0, pushthrough=True): if len(x): if type(x[0]) is list: x = [v for sample in x for v in sample] - if self.channel_format == cf_string: + if self.channel_format == ChannelValueFormats.STRING.value: x = [v.encode("utf-8") for v in x] if len(x) % self.channel_count == 0: # x is a flattened list of multiplexed values @@ -914,7 +967,7 @@ class StreamInlet: """ def __init__( - self, info, max_buflen=360, max_chunklen=0, recover=True, processing_flags=0 + self, info, max_buflen=360, max_chunklen=0, recover=True, processing_flags=PostProcessingFlags.NONE.value ): """Construct a new stream inlet from a resolved stream description. @@ -1091,7 +1144,7 @@ def pull_sample(self, timeout=FOREVER, sample=None): handle_error(errcode) if timestamp: sample = [v for v in self.sample] - if self.channel_format == cf_string: + if self.channel_format == ChannelValueFormats.STRING.value: sample = [v.decode("utf-8") for v in sample] if assign_to is not None: assign_to[:] = sample @@ -1159,7 +1212,7 @@ def pull_chunk(self, timeout=0.0, max_samples=1024, dest_obj=None): [data_buff[s * num_channels + c] for c in range(num_channels)] for s in range(int(num_samples)) ] - if self.channel_format == cf_string: + if self.channel_format == ChannelValueFormats.STRING.value: samples = [[v.decode("utf-8") for v in s] for s in samples] free_char_p_array_memory(data_buff, max_values) else: @@ -1474,6 +1527,24 @@ def handle_error(errcode): lost_error = LostError vectorf = vectord = vectorl = vectori = vectors = vectorc = vectorstr = list +# Recommended to use the ChannelValueFormats Enum class +cf_float32 = 1 +cf_double64 = 2 +cf_string = 3 +cf_int32 = 4 +cf_int16 = 5 +cf_int8 = 6 +cf_int64 = 7 +cf_undefined = 0 + +# Recommended to use the PostProcessingFlags Enum class +proc_none = 0 +proc_clocksync = 1 +proc_dejitter = 2 +proc_monotonize = 4 +proc_threadsafe = 8 +proc_ALL = (proc_none | proc_clocksync | proc_dejitter | proc_monotonize | proc_threadsafe) + def resolve_stream(*args): if len(args) == 0: