make4all / psst

Repository for sonification
https://make4all.github.io/psst/
MIT License
4 stars 1 forks source link
accessibility sensors sonification visualization

Physical Computing Streaming Sensor Tooltip (PSST)

The Physical computing Streaming Sensor Sonification Toolkit (PSSST) is designed to simplify the authoring of sonification of live sensor data for the purposes of understanding, testing, and debugging sensors. The toolkit is designed more generally to support authoring across different types of streaming data, but has prioritized features that will specifically be helpful when dealing with multiple sensor data streams.

The basic architecture of PSSST is strea based. Two different streams are needed to visualize data:

These are merged together into [OutputStateChange, Datum] tuples and passed various parts of the PSSST architecture subscribe to this stream and act on it. More details on all of this below.

Installation and development

  1. Install node.js and yarn
  2. Install the required packages with
yarn install
  1. Start the development server
yarn start

Basic Usage

We start with a simple example of how to use PSSST. This example is a summary of what is done in DemoSimple.tsx. We break this up according to the two streams described above, along with sonification authoring.

DemoSimple is a very simple demonstration app that allows the user to pause and play data from a fixed CSV file at a fixed rate (once every 500 ms)

[OutputStateChange] Stream

DemoSimple provides the user with an interface for playing and pausing data streaming, and this same interface is used to generate the stream of OutputStateChange events. To do this, DemoSimple must

Display Authoring

To author a sonification, or any other display of information,DemoSimple must specify a few key pieces of information:

[Datum] Streaming

DemoSimple also needs to stream data to the OutputEngine. In DemoSimple we do this by creating a stream from the original array of data as follows:

let data$ = of(...data)
        let timer$ = timer(0, 500).pipe(debug(SonificationLoggingLevel.DEBUG, 'point number'))
        let source$ = zip(data$, timer$, (num, time) => new Datum(id, num)).pipe(
            debug(SonificationLoggingLevel.DEBUG, 'point'),
        )

Once a stream exists (whereever it comes from), it is a simple matter of asking the OutputEngine to pipe it into a specific DataSink as follows: OutputEngine.getInstance().setStream(id, source$).

The OutputEngine class

The OutputEngine class is designed to only ever have a single instance. This means that even as the user flips from one webpage to another there is still a way to keep outputs running (or turn them off, or change their volume). It also handles glabel needs like pausing the audio for all data sinks, generating a new data sink class with a unique ID as needed, adjusting overall volume (still TBD).

It's primary purpose, however, is to merge the incoming global stream of OutputStateChange events with each DataSink's individual stream of Datum events and to keep track of all the sinks and their streams.

The DataSink class

Data sinks are holders for a specific data sink. For the most part, our current implementation assumes each DataSink class will handle only a single stream of timestamped streaming numbers (specifically, streames containing the Datum class. As described above, streaming data can either be provided as an Rxjs Observable or through a call to OutputEngine.pushPoint().

Data sinks contain a chain of Handler classes which handle incoming data. Handlers are described more in depth below, but they may filter data, calculate an adjusted value for data, and/or cause data to be outputed. They can be thought of as dispatch handlers for data. The Data sink ensures that these handlers are chained together, so that if one handler receives a stream of data and filters it, the next handler will only see the filtered version of the data. A Data sink also has a name, and a unique ID, which it enforces is added to every incoming data point.

Datum

A Datum is just a single item of a data stream. It knows which sink it is being handled by, and has a raw value, a timestamp, and an adjusted value. Some outputs may use the adjusted value instead of the raw value to decide how to render the Datum.

Handlers

A Handler is an abstract class that is designed to handle streaming data. Handlers are chained together by the DataSink class, and any handler can also have an DataOutput class that displays the data the handler is streaming.

A DataHandler should behave in the following ways

A handler may have one or more DatumOutput classes which are used to render the Datum. Each of these will be subscribed to the handler's output.

Some examples may help to clarify how Handler classes work.

We haven't implemented all handlers. Some additional ones that we are thinking of adding:

Outputs

The same output can be used by more than one handler. We currently include only outputs that sonify data with nonspeech audio, though we plan to add a speech output. They all inheret from Sonify, which in turn inherets from DatumOutput. We currently support the following

The base class for outputs processes the data stream and calls start(), stop(), and pause() when there is a new OutputStateChange event. It also calls output(datum) when state is OutputStateChange.Playing. Subclasses only need to implement these methods to function properly.

Statistics

Statistics are used to calculate information about a stream, such as the maximum number seen so far, or a running average. They may be used by DatumOutput or DataHandler classes to make decisions about things (such as what range to filter over).

The base statistic class strips everything out of the stream except the number inside the Datum object, so that stats calculations can just work on pure streams of numbers, to simplify things.

Currently the only implemented statistics are:

       bufferCount(this.buffer),
                map((frames) => {
                    const total = frames.reduce((acc, curr) => {
                        acc += curr
                        return acc
                    }, 0)
                    return 1 / (total / frames.length)
                }),
         reduce((acc, curr) => {
                    if (this.direction == GrowthDirection.Min) {
                        return curr >= acc ? acc : curr
                    } else {
                        return curr <= acc ? acc : curr
                    }
                }),

However we have discussed other ideas for statistics such as an Extreme (could keep track of outliers in the data stream)