Processing Expert Article 7

Live Performance & VJing

Build a modular VJ sketch with scene switching, MIDI parameter control, Syphon/Spout video output, and cross-fade transitions.

⏱ 55 min VJing Syphon Spout MIDI live performance 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.