goofi v3.0.0

A biofeedback framework for explorative experiments.

Rapidly prototype the entire biofeedback loop end-to-end. goofi handles signal processing, feature extraction, as well as audiovisual stimulus generation, all in one modular node patching editor. Don't know how? Your agent will know due to native AI agent integration.

Node connections Inputs on the left · outputs on the right
LslIn py
out
Psd rs
input
out
EegPowerBands py
psd
power
SignalIn rs
input
out
AudioOut rs
input

Three engines, and your plugins.

Each node runs at its own pace: a kilohertz EEG stream and an audio chain share a patch without either holding the other up. Several people can work in it at once, each with their own undo. Building and exploring become the same act, and what you take away is intuition about your signal and the features you pull from it.

What goes into a goofi patch, which engine takes it, and what comes outLive input on one side, each coloured by the engine it binds to: EEG and physiology, sensor streams and a hand on a control to the signal engine, audio and MIDI to the audio engine. The three engines sit together in the centre, with the VST3 plugin tab beside them. Output on the other side, each coloured by the engine that makes it: generative visuals from graphics, sound and music from audio, and measures, text and streams back out from signal.signalaudiovstgraphicsgoofi
Signal

Frames of array data at whatever rate the source runs. LSL, MIDI and OSC in and out, spectra and band powers, complexity measures, and the arithmetic to select, reshape, combine and route whatever comes out of them.

Audio

The same nodes at audio rate. Live in and out, MIDI, oscillators, filters, envelopes and feedback.

Graphics

Frames of pixels on the GPU. A node is a WGSL fragment shader: noise, ramps and shapes to make them, blur, levels, displacement and compositing to work them, and feedback. A signal frame comes in through ArrayIn, as pixels, a line plot or a trajectory.

VST3

Any VST3 on the machine, as an audio node: goofi finds the plugins your system has installed. A plugin opens its own editor window, and its parameters are the node’s, so a band power can drive one.

Your agent, inside the patch.

goofi embeds the terminal agent you already use — claude, codex and opencode out of the box, and any other command you name. It opens as a panel beside the canvas, starts in the patch's workspace, and reads the AGENTS.md there to orient itself. You talk to it the way you always do.

It reaches everything the editor does: add nodes, wire them, bind a parameter, lay out the panels, save the patch. Every edit it makes shows up as it happens, so you watch the patch build itself and take the mouse back whenever you like — its work lands on the same undo stack as yours.

The same patch, from a shell.

Everything the editor does is an op with a name, and the goofi binary speaks them to a running goofi. Add a node, wire it, set a parameter, lay out a panel; the canvas follows as you type. A file of ops is a script, and a script lands as one undo step.

The CLI → · The full op vocabulary →

a shell beside a running goofi
$ goofi node add signal:Psd
$ goofi link add lslin.out psd.data
$ goofi layout panel edit p2 --type viewer
$ goofi - < ops.txt      # a batch from stdin, one undo step

Write a file, get a node.

Drop a Python, Rust or WGSL file in a folder and it is in the palette. goofi brings its own Python, so there is nothing to install first, and a parameter can be a Python expression.

How to write a node →

nodes_signal/smooth.py
import goofi
import numpy as np


class Smooth(goofi.Node):
    """Rolling mean over the last axis."""

    INPUTS = {"data": goofi.InputSlot(goofi.DataType.ARRAY, required=True)}
    OUTPUTS = {"out": goofi.DataType.ARRAY}
    PARAMS = {"smoothing": {"window": goofi.IntParam(8, 1, 512, doc="Samples in the mean.")}}

    def process(self, data):
        w = self.params.smoothing.window
        kernel = np.ones(w, dtype=np.float32) / w
        return np.apply_along_axis(lambda v: np.convolve(v, kernel, mode="same"), -1, data.data)