Live Performance & VJing
Build a modular VJ sketch with scene switching, MIDI parameter control, Syphon/Spout video output, and cross-fade transitions.
Live visual performance demands resilience: no crashes, no loading delays, instantaneous response to incoming triggers. A well-designed VJ sketch never hard-restarts mid-set. Everything is reachable via MIDI or keyboard without touching code.
Sketch Architecture for Live Use
The central design principle is separation of concerns. Each visual scene is a self-contained object. A top-level manager handles scene switching, transitions, and MIDI routing. Scenes are instantiated once at startup and never deallocated.
Scene[] scenes = new Scene[4];
int current = 0;
int next = -1; // -1 = no transition in progress
float xfade = 0; // 0.0–1.0 cross-fade position
PGraphics bufCurrent, bufNext;
void setup() {
size(1920, 1080, P2D);
frameRate(60);
bufCurrent = createGraphics(width, height, P2D);
bufNext = createGraphics(width, height, P2D);
scenes[0] = new GridScene();
scenes[1] = new TunnelScene();
scenes[2] = new ParticleScene();
scenes[3] = new LissajousScene();
for (Scene s : scenes) s.setup(width, height);
}
Scene Interface
Define a simple interface that all scenes implement:
interface Scene {
void setup(int w, int h);
void update();
void draw(PGraphics g);
void onTrigger(int noteOrCC, float value); // MIDI input
void reset(); // called on scene exit
}
A minimal example scene:
class GridScene implements Scene {
float phase = 0;
float density = 20;
float speed = 0.03;
public void setup(int w, int h) {}
public void update() {
phase += speed;
}
public void draw(PGraphics g) {
g.beginDraw();
g.background(10);
g.stroke(80, 200, 255, 180);
g.strokeWeight(1);
int cols = (int)density;
float cw = g.width / (float)cols;
float rh = g.height / (float)cols;
for (int x = 0; x < cols; x++) {
for (int y = 0; y < cols; y++) {
float cx = x * cw + cw/2;
float cy = y * rh + rh/2;
float r = 20 + 18 * sin(phase + x * 0.4 + y * 0.3);
g.ellipse(cx, cy, r, r);
}
}
g.endDraw();
}
public void onTrigger(int n, float v) {
if (n == 1) speed = map(v, 0, 127, 0.005, 0.12);
if (n == 74) density = map(v, 0, 127, 5, 40);
}
public void reset() { phase = 0; }
}
Cross-Fade Transitions
Transitions happen over a fixed number of frames. During the transition, both scenes render to their own PGraphics buffers and the result is alpha-blended on screen.
final int FADE_FRAMES = 30;
int fadeTimer = 0;
void startTransition(int nextIndex) {
if (next != -1) return; // transition already in progress
next = nextIndex;
fadeTimer = 0;
xfade = 0;
scenes[next].reset();
}
void draw() {
scenes[current].update();
if (next != -1) {
scenes[next].update();
fadeTimer++;
xfade = (float)fadeTimer / FADE_FRAMES;
scenes[current].draw(bufCurrent);
scenes[next].draw(bufNext);
// Composite
image(bufCurrent, 0, 0);
tint(255, (int)(xfade * 255));
image(bufNext, 0, 0);
noTint();
if (fadeTimer >= FADE_FRAMES) {
current = next;
next = -1;
xfade = 0;
}
} else {
scenes[current].draw(bufCurrent);
image(bufCurrent, 0, 0);
}
}
Hot Parameter Control via Keyboard
Assign keys to parameters so you can tweak without a MIDI controller:
float globalSpeed = 1.0;
float globalBrightness = 1.0;
void keyPressed() {
switch (key) {
case '1': startTransition(0); break;
case '2': startTransition(1); break;
case '3': startTransition(2); break;
case '4': startTransition(3); break;
case '+': globalSpeed = min(globalSpeed + 0.1, 3.0); break;
case '-': globalSpeed = max(globalSpeed - 0.1, 0.1); break;
case 'b': globalBrightness = min(globalBrightness + 0.05, 1.0); break;
case 'v': globalBrightness = max(globalBrightness - 0.05, 0.0); break;
case 'r': scenes[current].reset(); break;
case 'f': saveFrame("capture/frame-####.png"); break;
}
}
Syphon (macOS) Output
Syphon shares the sketch’s OpenGL texture with other applications on macOS without a performance penalty. Install the Syphon for Processing library:
import codeanticode.syphon.*;
SyphonServer syphon;
void setup() {
size(1920, 1080, P3D); // Syphon requires P3D or P2D
syphon = new SyphonServer(this, "Processing VJ");
}
void draw() {
// ... draw your scene ...
syphon.sendScreen(); // shares current frame via Syphon
}
In Resolume Avenue or VDMX, add a Syphon input source and select “Processing VJ”.
Spout (Windows) Output
Spout is the Windows equivalent. Install the Spout for Processing library by Florian Bruggisser:
import spout.*;
Spout spout;
void setup() {
size(1920, 1080, P3D);
spout = new Spout(this);
spout.createSender("Processing VJ");
}
void draw() {
// ... draw your scene ...
spout.sendTexture();
}
The Spout sender appears in Resolume, MadMapper, or any Spout-compatible receiver on the same machine.
Multiple Output Windows
Processing 4 can open additional windows for multi-projector setups via PSurface:
PApplet secondWindow;
void setup() {
size(1920, 1080, P2D);
// Launch a second sketch instance on another display
String[] secondArgs = {"SecondWindow"};
secondWindow = new SecondWindowSketch();
PApplet.runSketch(secondArgs, secondWindow);
}
For pixel-mapped LED arrays or theatre-grade multi-projector rigs, consider the GLVideo or GStreamer path which allows per-frame texture streaming to multiple physical outputs without multiple windows.
Full Example: Modular VJ Sketch with 4 Scenes and MIDI Mapping
import themidibus.*;
import codeanticode.syphon.*;
MidiBus bus;
SyphonServer syphon;
Scene[] scenes = new Scene[4];
int current = 0, next = -1;
int fadeTimer = 0;
final int FADE_FRAMES = 24;
PGraphics bufA, bufB;
void setup() {
size(1920, 1080, P2D);
frameRate(60);
MidiBus.list();
bus = new MidiBus(this, 0, -1); // input 0, no output
syphon = new SyphonServer(this, "ProcessingVJ");
bufA = createGraphics(width, height, P2D);
bufB = createGraphics(width, height, P2D);
scenes[0] = new GridScene();
scenes[1] = new TunnelScene();
scenes[2] = new ParticleScene();
scenes[3] = new LissajousScene();
for (Scene s : scenes) s.setup(width, height);
}
void draw() {
scenes[current].update();
if (next != -1) scenes[next].update();
if (next == -1) {
scenes[current].draw(bufA);
image(bufA, 0, 0);
} else {
fadeTimer++;
float t = constrain((float)fadeTimer / FADE_FRAMES, 0, 1);
scenes[current].draw(bufA);
scenes[next].draw(bufB);
image(bufA, 0, 0);
tint(255, (int)(t * 255));
image(bufB, 0, 0);
noTint();
if (fadeTimer >= FADE_FRAMES) {
current = next;
next = -1;
fadeTimer = 0;
}
}
syphon.sendScreen();
surface.setTitle(nf(frameRate, 0, 1) + " fps Scene " + current);
}
// MIDI: notes 36–39 switch scenes; CC1 = scene speed; CC74 = density
void noteOn(int ch, int pitch, int vel) {
int idx = pitch - 36;
if (idx >= 0 && idx < scenes.length && idx != current) {
next = idx;
fadeTimer = 0;
scenes[next].reset();
}
scenes[current].onTrigger(pitch, vel);
}
void controllerChange(int ch, int number, int value) {
scenes[current].onTrigger(number, value);
}
void keyPressed() {
if (key >= '1' && key <= '4') {
int idx = key - '1';
if (idx != current) {
next = idx; fadeTimer = 0; scenes[next].reset();
}
}
if (key == 'f') saveFrame("frames/####.png");
}
Each scene class implements the four Scene interface methods. Scene objects are never destroyed mid-set, so their internal state (particle positions, oscillator phases) persists until reset() is explicitly called. This means switching from Scene 1 to Scene 3 and back returns Scene 1 to exactly where it was — a useful property for live improvisation.