Nodes/ꘇ Genereti/ꘇ chop.expression
ComfyUI Node

ꘇ chop.expression

Sine waves, maths and a text box

By languel·Created about a month ago·Updated 3 days ago· 1
ꘇ chop.expression
  • input
  • controls
  • channels
  • value
◄expressionsin(t*tau + c)►
◄channels1►
◄samples1►
◄sample_rate60.00►
◄time0.00►
◄performance{}►
◄offset_x0.00►
◄offset_y0.00►
◄offset_z0.00►
◄offset_t0.00►
◄parameters{}►

ꘇ chop.expression evaluates a small arithmetic expression once per sample, per channel, and hands you the result as a control signal. The default is sin(t*tau + c) - a sine wave where each channel gets its own phase offset. It's the node you reach for when you want a parameter to move in a way an LFO preset can't quite do.

What it is

Genereti's chop.* family is a control-signal layer for ComfyUI, borrowed straight from TouchDesigner's vocabulary: a CHOP is a named block of float samples with a sample rate, and any numeric widget on any node can be converted to an input and driven by one. chop.expression sits at the flexible end of that family, next to chop.constant (fixed), chop.lfo (oscillating) and chop.lag (smoothed).

Why a text box instead of a knob: because an expression can read the project clock, so you tie a parameter to musical position rather than to a stopwatch.

How it works

Two modes, decided by whether input is connected:

  • Generate mode (no input) - it writes a fresh signal from scratch. t is the sample time, i the sample index, and x the normalised sample coordinate across the block (0 to 1). w is the block size, h the channel count, and c is the index of the channel being computed, so sin(t*tau + c) gives you channels that are out of phase with each other - exactly what you want for per-channel colour or rotation.
  • Process mode (input wired) - it computes per channel from the incoming data. v is the current input value and a is an alias for it, t runs from the input block's start time, and channel names are preserved through to the output. That's how you reshape an analysis tap or a gesture.

The parser is deliberately not JavaScript. The docs describe it as a bounded arithmetic parser: numbers, operators, and a fixed function set - sine, cosine, and the coherent-noise helpers perlin, simplex and value, which are the ones to use when you want noise rather than flicker. It also exposes the project performance fields, so __.beat, __.bar, __.bpm, __.ticks, __.phase and __.playing (as 0 or 1) are readable inside the expression, even when the node's own t is running in free time.

That distinction matters and is easy to miss: local t is the node's own time domain, editable via time, offset_t, sample_rate and samples; __.time always reads the project clock.

The inputs and outputs that matter

The required widgets: expression (multiline, default sin(t*tau + c)), channels (1–64), samples (1–4096, default 1), sample_rate (default 60), time (default 0). The samples and sample_rate tooltips are the honest ones from the pack: sample_rate is samples per second, not blocks per second, and live updates per second = sample_rate / samples, limited by browser frames. Use samples = 1 for responsive scalar control; 8 at 60 Hz gives you 7.5 blocks per second.

Optional inputs: input (the CHOP to process), performance (a JSON clock snapshot from a transport), the four domain-offset ports offset_x, offset_y, offset_z, offset_t, plus parameters and controls - the mechanism for naming values and driving them from other nodes instead of baking constants into the text.

offset_x shifts the normalised sample coordinate, y and z supply extra domain coordinates (feed them into noise functions), and offset_t adds seconds to the sample time. This is what lets you animate a noise field rather than watch a static slice of it.

Outputs: channels (the block) and value (a FLOAT - the last sample of the first channel). Wire value into a converted numeric widget; use channels when the signal needs to be part of a bigger control chain.

Install

Manager → search Genereti - or:

git clone https://github.com/languel/genereti.git custom_nodes/genereti
./.venv/bin/python -m pip install -r custom_nodes/genereti/requirements.txt

Restart ComfyUI, then reload the browser. requirements.txt is a single line (comfyui-frontend-package>=1.53.6); NumPy and the rest come from ComfyUI, and there's nothing to download. Comfy ≥ 0.38.2, Python ≥ 3.11. Restarting matters here more than usual because the expression text box is a custom frontend widget.

Common issues

The expression is rejected or silently returns zero. The parser is bounded arithmetic, not a scripting language - no variable assignments, no object access, no if. Rewrite conditionals as arithmetic (multiply by a comparison, use abs, or clamp via a downstream chop.math with the clamp operation).

Nothing moves while the workflow runs. Live evaluation happens in the browser; queued runs evaluate explicit samples and time. If you're queueing and expecting continuous drift, that's the mismatch.

c does nothing visible. c is the channel index and only pays off when channels is greater than 1; with one channel it's a constant.

Noise looks like mush. Use perlin/simplex/value for coherent motion, and animate it by feeding offset_t from a clock rather than cranking speed. At the default samples = 1 you're evaluating single points, so the block-wide helpers (w, x, i) only earn their keep at higher samples.

Timing drifts when the tab is backgrounded. Browser throttling can interfere with control updates and event lookahead; keep the tab focused when you're timing by eye.

And the standing caveat for this pack: solo MIT alpha by Liuboto, v0.1.4, "super alpha - early classroom testing", with a warning to back up workflows before updating.

CategoryGenereti / CHOP

Inputs (13)

NameTypeDefaultDescription
expressionSTRINGsin(t*tau + c)—
channelsINT11–64—
samplesINT11–4096Samples per delivered block. Live updates/s = sample_rate / samples, limited by browser frames. Use 1 for responsive scalar control; 8 at 60 Hz delivers 7.5 blocks/s.
sample_rateFLOAT60.001–1000Samples per second, not blocks per second. A block contains samples values; use samples=1 for updates near this rate.
timeFLOAT0.00-100000–100000—
inputoptGENERETI_CHOP—
performanceoptSTRING{}—
offset_xoptFLOAT0.00-100000–100000—
offset_yoptFLOAT0.00-100000–100000—
offset_zoptFLOAT0.00-100000–100000—
offset_toptFLOAT0.00-100000–100000—
parametersoptSTRING{}—
controlsoptCOMFY_AUTOGROW_V3—

Outputs (2)

NameTypeDescription
channelsGENERETI_CHOP—
valueFLOAT—