Processor Plugins#

Processor plugins are the key elements of the GUI’s signal chain; they respond to and update continuous data, spikes, and events inside their process() method.#

Type

Plugin::Type::PROCESSOR

Base Classes

GenericProcessor, GenericEditor

Template

open-ephys-plugins/processor-plugin-template

Overview#

If you’re developing a new plugin for the GUI, it will most likely be a Processor Plugin. The core functionality of these plugins is defined in their process() method, which determines how they generate, modify, or respond to data passing through the GUI’s signal chain.

There are three type of Processor Plugins:

  1. Sources generate data, and must be placed at the beginning of a signal chain. Note that if a source is communicating with external hardware that is not synchronized with the GUI’s signal chain callbacks, a Data Threads plugin should be used instead.

  2. Filters modify data, either by changing the samples contained in the continuous channel data buffer or adding TTL events or spikes to the event bufer. Filters must receive data from at least one Source.

  3. Sinks send data outside the signal chain, without modifying it in any way. Sinks are typically used for visualizing data or sending triggers to external hardware.

Processor Plugins must also implement an “Editor” derived from the GenericEditor class which contains various UI elements for changing the plugin’s parameters.

In addition, Processor Plugins can be “Visualizers” if they include a canvas for displaying data and/or parameter interfaces. See the Visualizer Plugins page for more information about the API methods related to visualization.

Key methods#

Constructors#

GenericProcessor constructor#

The plugin class constructor should look like this:

ProcessorPlugin::ProcessorPlugin()
    : GenericProcessor ("Plugin Name")
{

    // add plugin parameters here
}

The GenericProcessor constructor includes one parameter that specifies how the plugin’s name will appear in the GUI’s Processor List. The constructor should also initialize any Parameter objects that the plugin will use (see below for more info).

GenericEditor constructor#

The constructor for the plugin’s editor class must also be defined as follows:

ProcessorPluginEditor::ProcessorPluginEditor(GenericProcessor* parentNode)
    : GenericEditor(parentNode)
{
    desiredWidth = 150;

    // add parameter editors here
}

You can change the value of desiredWidth to specify how wide (in pixels) the plugin’s editor should be. This value can be changed later on if the editor needs to expand or contract. The editor constructor must define any ParameterEditor widgets that will be used. The underlying parameters must have already been defined in the plugin’s constructor.

Next, you’ll need to make sure the plugin’s createEditor() method is correct:

AudioProcessorEditor* ProcessorPlugin::createEditor()
{
    editor = std::make_unique<ProcessorPluginEditor>(this);
    return editor.get();
}

Note

ProcessorPluginEditor” should be changed to match the name of your plugin’s editor class.

Updating plugin settings#

Whenever the signal chain is modified, the GUI will call updateSettings() on all plugins downstream of the modification, to allow them to respond to changes and inform downstream plugins about their current state:

void updateSettings()#

Processor plugins should override this method in order to respond to configuration information about upstream plugins, and ensure their own configuration information is sent to downstream plugins.

The following internal variables are used to pass information between plugins prior to the start of acquisition:

  • dataStreams - A Juce OwnedArray that stores pointers to the available DataStream objects that will be processed by this plugin. Each DataStream is assigned a uint16 identifier that is guaranteed to be unique within a given run of the GUI, but does not persist between runs.

  • continuousChannels - A Juce OwnedArray that stores pointers to the available ContinuousChannel objects that will be processed by this plugin. Each ContinuousChannel must be associated with a DataStream containing all of the other channels that are sampled synchronously.

  • eventChannels - A Juce OwnedArray that stores pointers to the available EventChannel objects that will be processed by this plugin. Similar to ContinuousChannel objects, each EventChannel must be associated with one and only one DataStream. Note that each EventChannel object can track state changes across many TTL “lines.”

  • spikeChannels - A Juce OwnedArray that stores pointers to the available SpikeChannel objects that will be processed by this plugin. Each SpikeChannel must be associated with one and only one DataStream, and will also contain pointers to the ContinuousChannel objects that represent the continuous channels from which spikes are samples. A SpikeChannel will generate spikes that are associated with one “Electrode” (e.g. single electrode, stereotrode, or tetrode).

Important

DataStream, ContinuousChannel, EventChannel, and SpikeChannel objects do not persist between calls to updateSettings(). Do not store pointers to these objects outside the standard locations, as the underlying objects will be deleted the next time the signal chain is modified.

If a plugin will generate continuous, event, or spike data, it must create new channel info objects and add them to the appropriate DataStream inside the updateSettings() method.

Plugins that only generate events can call the following method inside updateSettings() to automatically add an event channel to the first incoming DataStream:

void addTTLChannel(String name)#

Adds a TTL event channel to the first incoming data stream, so downstream plugins can respond to events generated by this plugin. This method should only be called once, inside the updateSettings() method. Note that each “channel” can contain state information for up to 256 TTL lines.

Parameters:

name – The name of this channel. Channel names must be unique for each data stream within a particular plugin.

Starting/stopping acquisition#

If a plugin needs to update its internal state just before the start of acquisition, it should override the following method:

bool startAcquisition()

Informs a plugin that acquisition is about to begin. If a plugin is not ready to handle incoming data, it can return false to prevent acquisition from starting.

bool stopAcquisition()

Informs a plugin that acquisition has stopped. The return value is not currently handled.

Processing data#

The process() method is where a Processor Plugin’s core functionality is defined:

void process(AudioBuffer<float> buffer)#

Allows a plugin to modify a buffer of continuous channels, and add events or spikes that will be received by downstream plugins. This method is called repeatedly whenever acquisition is active.

Parameters:

buffer – A Juce AudioBuffer containing data samples for all continuous channels processed by this plugin. The ordering of channels in this buffer matches the ordering of channels in the continuousChannels array. Since this buffer contains samples for all incoming data streams, be sure to call getGlobalIndex() for each continuous channel to find its position in this buffer, which may be different from its position within its data stream. In general, this buffer stores data in units of microvolts, but there are some cases (such as ADC channels) where the data is stored in volts.

Continuous data#

Before performing any read/write operations on a continuous channel, it is necessary to query the number of valid samples that are available in a given callback:

int getNumSamplesInBlock(uint16 streamId)#

Returns the number of samples available for a stream. Must be called on a per-stream basis, because different streams are not guaranteed to have the same number of samples in each buffer.

Parameters:

streamId – The ID of the data stream to check.

Returns:

The number of samples available in the current buffer for the specified stream.

Example:

See the FilterNode::process() method.

A plugin should never request samples that are above the index returned by this method. This method can sometimes return 0, if there are no samples available for a given data stream.

In order to read or write data from the continuous buffer, the following methods should be used:

float *getReadPointer(int globalChannelIndex)#

Returns a pointer to the data for one channel; only use this if the plugin will not overwrite the continuous data buffer.

Parameters:

globalChannelIndex – The global index of the continuous data channel

Returns:

A pointer to the continuous data.

Example:

See the PhaseDetector::process() method.

float *getWritePointer(int globalChannelIndex)#

Returns a pointer to the data for one channel; only use this if the plugin will overwrite the continuous data buffer.

Parameters:

globalChannelIndex – The global index of the continuous data channel to modify.

Returns:

A pointer to the continuous data.

Example:

See the FilterNode::process() method.

Spike and event data#

To notify the GUI that the plugin needs to respond to incoming events and spikes within the process() method, the following method must be called:

int checkForEvents(bool respondToSpikes)#

Indicates that the plugin wants to respond to incoming events and/or spikes.

Parameters:

respondToSpikes – Set to true if the plugin needs to receive spikes as well.

Returns:

0 if there are events available in this buffer, -1 otherwise.

The plugin should override the following methods to actually deal with incoming events and spikes:

void handleTTLEvent(TTLEventPtr event)#

Passes the next available incoming event to the plugin.

Parameters:

event – Pointer to a TTLEvent object containing information about this event. This includes the event channel that generated it, the ID of the data stream it is associated with, the line on which the event occurred, and the sample number of the event (relative to the start of acquisition).

Example:

See the ArduinoOutput::handleEvent() method.

void handleSpike(SpikePtr event)#

Passes the next available incoming spike to the plugin.

Parameters:

event – Pointer to a Spike object containing information about this spike. This include the spike channel that generated it, the ID of the data stream it is associated with, the sample number of the event (relative to the start of acquisition), and the full spike waveform (in units of microvolts).

Example:

See the SpikeDisplayNode::handleSpike() method.

Assuming that addTTLChannel() was called inside the process() method (see above), a plugin can add events using the following method:

void flipTTLState(int sampleIndex, int lineIndex)#

Adds an event indicating a state change on a particular TTL line.

Parameters:
  • sampleIndex – The sample index (relative to the start of the current block of the first incoming stream) at which this state change occurred.

  • lineIndex – The TTL line on which the state change occurred (0-255).

void setTTLState(int sampleIndex, int lineIndex, bool state)#

Adds an event with a specified state value (ON or OFF). Note that consecutive “ON” or “OFF” events are valid within software, even if they would be impossible to generate using actual hardware.

Parameters:
  • sampleIndex – The sample index (relative to the start of the current block of the first incoming stream) at which this state change occurred.

  • lineIndex – The TTL line on which the state change occurred (0-255).

  • state – The state of the TTL line (ON = true, OFF = false).

Sending and receiving messages#

While acquisition is not active, plugins can respond to configuration messages and send status messages:

void handleConfigMessage(String message)

Allows a plugin to respond to a configuration message (usually received via the OpenEphysHTTPServer). This makes it possible to configure a plugin’s settings remotely. Config messages are ignored if acquisition is active.

param message:

The content of the configuration message. There are no restrictions on how this string is formatted; each plugin is responsible for parsing this message in the appropriate way.

void CoreServices::sendStatusMessage(String message)#

Displays a message to the user in the GUI’s Message Center.

param message:

The message to display.

While acquisition is active, plugins can respond to and send broadcast messages:

void handleBroadcastMessage(String message)

Allows a plugin to respond to an event that carries a text value, which is broadcast throughout the signal chain during acquisition. These messages can be used to pass information backwards through the signal chain, e.g. to trigger an output based on events that are generated downstream.

Parameters:

message – The content of the broadcast message. There are no restrictions on how this string is formatted; each plugin is responsible for parsing this message in the appropriate way.

void broadcastMessage(String message)

Can be called during the process() method to send a message to all other plugins in the signal chain. These messages will be automatically saved by any Record Nodes in the signal chain.

Parameters:

message – The content of the broadcast message. There are no restrictions on how this string is formatted; each plugin is responsible for generating messages that can be parsed by other plugins.

Plugin parameters#

The GUI’s built-in Parameter class provides an easy way to manage the parameters for your plugin. Using this class provides the following advantages:

  • Parameter names, default values, and ranges are defined with one line of code

  • Parameter editors can be auto-generated with a second line of code

  • Parameters values can be safely updated while acquisition is active

  • The GUI will automatically track parameters across different data streams

  • Parameter values will be automatically saved and loaded

Defining parameters#

All parameters must be defined inside your plugin’s class constructor, using the constructor methods for different types of parameters, e.g.:

void addIntParameter(Parameter::ParameterScope scope, const String &name, const String &description, int defaultValue, int minValue, int maxValue)#

Adds a parameter that can take integer values.

Parameters:
  • scope – Use Parameter::GLOBAL_SCOPE if the parameter will hold a single value each plugin or Parameter::STREAM_SCOPE if the parameter can be changed independently for each incoming data stream.

  • name – Name of the parameter (cannot have spaces)

  • description – A one-line description of this parameter

  • defaultValue – The default value for this parameter

  • minValue – The minimum value this parameter can take

  • maxValue – The maximum value for this parameter

See GenericProcessor.h for a complete list of Parameter constructors.

Accessing parameters#

For global parameters, use the following method:

Parameter *getParameter(String name)#

Returns a pointer to a global parameter.

Parameters:

name – The name of the parameter.

The easiest way to access stream-specific parameters is through the overloaded bracket operator:

(*stream)["parameter_name"]->getValue();

In both cases, requesting a parameter name that doesn’t exist will result in a segfault.

Creating parameter editors#

Parameters can be modified using editors that are auto-generated by the GUI. These must be initialized in the class constructor for the plugin’s editor, e.g.:

void addTextBoxParameterEditor(const String &name, int xPos, int yPos)#

Adds a text box that can be used to modify the values of a Parameter object.

Parameters:
  • name – The name of the parameter. This must match the name of a parameter that has been created inside the plugin’s constructor.

  • xpos – The horizontal position (in pixels) of this parameter editor within the plugin’s editor (left edge = 0)

  • ypos – The vertical position (in pixels) of this parameter editor within the plugin’s editor (top edge = 0)

See GenericEditor.h for a complete list of Parameter constructors.

Responding to parameter value changes#

Your plugin can implement a custom response to parameter changes. For example, if the filter high cut changes and a filter needs to be updated. To do this, override this virtual method in your plugin.

void parameterValueChanged(Parameter *param) override#

Called whenever a parameter value changes.

Parameters:

param – A pointer to the parameter object that was updated.

Saving and loading custom parameters#

The GUI saves the signal chain in the following situations:

  1. Whenever a processor is added, moved, or deleted, the signal chain is written to recoveryConfig.xml

  2. Whenever a recording is started, the signal channel is written to settings.xml inside each Record Node directory

  3. Whenever the GUI is closed, the signal chain is written to lastConfig.xml

  4. Whenever the signal chain is cleared, the previous state is stored in memory so this action can be undone.

In addition, the settings for individual plugins are stored in memory whenever a plugin is copied.

If the plugin uses any parameters that are not use the built-in Parameter class, it needs to implement the following functions to ensure they are saved and loaded properly:

void saveCustomParametersToXml(XmlElement *xml)#

Used to save custom parameters.

Parameters:

xml – Pointer to an XmlElement that will store these parameters.

To add a parameter to the XmlElement, use the following code:

xml->setAttribute("parameterName", value);

The name string cannot have any spaces, and the value can be a boolean, integer, or string.

void loadCustomParametersFromXml(XmlElement *xml)#

Used to load custom parameters.

Parameters:

xml – Pointer to an XmlElement that was saved previously.

To read out the parameters, you can use the following methods:

int parameter1Value = xml->getIntAttribute("parameter1Name", 0);
bool parameter2Value = xml->getBoolAttribute("parameter2Name", false);
String parameter2Value = xml->getStringAttribute("parameter3Name", "default");

Be sure to supply a default value (the second argument), in case the parameter doesn’t exist in the config file being loaded.

Plugins can also save and load settings via their editors, by overriding the GenericEditor::saveCustomParametersToXml() and GenericEditor::loadCustomParametersFromXml() methods.