Real-time datamosh & glitch-art for desktop.
Tauri · p5.js · Rust · Syphon · Spout · MIDI · OSC
huff is a desktop application for creating real-time datamosh, glitch-art, and feedback effects. Load a video file or connect a webcam, then sculpt the image through a live effect chain — datamosh tile displacement, feedback loops, flow warps, symmetry folds, solarise, scanline corruption, and ghost trails.
It ships with a two-window architecture: a controls panel and a separate fullscreen output window that receives the processed frames over a local WebSocket relay. The output window can be recorded, mirrored to a projector, or shared directly to VJ software via Syphon (macOS) or Spout (Windows).
huff is built for performers and artists who want a tool that behaves predictably under pressure — MIDI-mappable, OSC-receivable, and hot-swappable mid-set.
- Features
- How it Works
- Installation
- Building from Source
- The Interface
- MIDI
- OSC
- Video Output
- Parameter Reference
- Presets Reference
- Architecture
- Performance Notes
- Caveats and Known Limitations
- Troubleshooting
- Datamosh / glitch tiles — temporal tile displacement sampled from a 60-frame ring buffer with cluster physics, spatial gap, smear, and depth scatter
- Feedback loop — zoom, translate X/Y, and rotate the buffer back onto itself each frame
- Flow warp — noise-field optical-flow UV distortion with pulse (sample from N frames back) and implosion modes
- Symmetry — vertical, horizontal, or both axes with a moveable split position
- Solarise — luminance-threshold colour inversion with per-channel R/G/B tinting
- Scanline bands — drifting horizontal displacement bands sampled from past frames, with gap quantisation and skew
- Trails — stacked ghost frames with luma keying for motion-smear
- Camera input — live webcam feed alongside or instead of video files
- Syphon output (macOS) — zero-install Metal texture sharing to Resolume, VDMX, MadMapper, CoGe, Millumin, and any Syphon receiver
- Spout output (Windows) — D3D11 texture sharing to Resolume Arena, TouchDesigner, MadMapper, and any Spout2 receiver
- MIDI — full CC/note input via native Rust
midir; JSON-format map files; supports hardware controllers and virtual ports - OSC — UDP listener on port 9000; JSON-format map files; works with TouchOSC, Max/MSP, Pure Data, SuperCollider, TouchDesigner
- Presets — save and recall named snapshots of all parameters; 7 factory presets included
- Two-window output — controls panel + separate fullscreen canvas window via embedded WebSocket relay
- Hot-key control —
Ptoggles the control panel;Ftoggles fullscreen on the output window
Each frame, huff runs the source material through up to eight independent effect passes. Each pass is optional and independently toggleable. The order is fixed — the output of one pass feeds the next.
┌──────────────────────────────────────────────────────────────────┐
│ SOURCE │
│ video file · webcam · frame ring seed │
└────────────────────────────┬─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ FRAME RING BUFFER │
│ Stores last N frames as ImageData (capped at 192 MB) │
│ N = quality × 60, max 60 frames at quality=1 │
└────┬──────┬──────┬──────┬──────┬──────┬──────┬──────────────────┘
│ │ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼ ▼
[t-1] [t-2] [t-3] ... [t-N]
│
▼ (each pass reads the ring; writes to gBuf)
┌─────────────────────────────────────────────────────────────────┐
│ PASS 1 TRAILS ghost frames composited under current │
│ PASS 2 FEEDBACK zoom/translate/rotate feedback blit │
│ PASS 3 GLITCH tile displacement from ring frames │
│ PASS 4 SCANLINES drifting horizontal band displacement │
│ PASS 5 FLOW WARP noise UV distortion │
│ PASS 6 SOLARIZE luma-threshold colour inversion │
│ PASS 7 SYMMETRY mirror fold at axis │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ OUTPUT FRAME │
├──────────────────────────────────────────────────────────────────┤
│ → Canvas window (WebSocket JPEG relay, port 8787) │
│ → Syphon server (macOS — Metal texture upload) │
│ → Spout sender (Windows — D3D11 texture upload) │
│ → Screen (controls window preview) │
└──────────────────────────────────────────────────────────────────┘
The controls window (index.html) runs inside the Tauri WebView. Every slider, checkbox, and input reads its value through the els object (a DOM lookup cache). The p5.js draw loop runs at the browser's frame rate and queries els directly — there is no intermediate state synchronisation layer.
┌──────────────────────────────────────────┐
│ Controls Panel (WebView) │
│ │
│ slider → input event │
│ ↓ │
│ els.corrupt.value (live DOM read) │
│ ↓ │
│ p5 draw() loop (requestAnimationFrame) │
│ ↓ │
│ applyGlitch(density, baseDX, baseDY) │
│ ↓ │
│ drawRingRegion() per tile │
└──────────────────────────────────────────┘
MIDI CC received → midir (Rust)
↓
Tauri "midi-event" event emitted
↓
canvas.js onmessage handler
↓
DOM element value updated
↓
Next draw() frame picks it up (no delay)
OSC UDP packet → rosc (Rust)
↓
Tauri "osc-message" event emitted
↓
canvas.js onmessage handler
↓
DOM element value updated ──→ same path as MIDI
MIDI and OSC both write to the same DOM elements as the sliders. There is intentionally no difference between moving a slider manually and receiving a CC or OSC message — they all converge on the same element value, and the draw loop reads that value on the next frame.
huff uses a single shared canvas as the source of truth for all output routes. The output pass happens at the end of each draw() call.
p5 gBuf (effect chain output)
│
├──→ Canvas window preview (drawn directly to screen)
│
├──→ WebSocket mirror (canvas.html)
│ JPEG-encoded at quality × 0.82
│ sent as binary WS message to port 8787
│ canvas.html drawImage() from blob URL
│
├──→ Syphon (macOS only, when enabled)
│ getImageData() → raw RGBA bytes
│ prepend "HUFFSYPH" + width + height header
│ binary WS to port 8787
│ Rust relay intercepts on b"HUFFSYPH"
│ syphon::push_frame() → MTLTexture upload
│ SyphonMetalServer.publishFrameTexture()
│
└──→ Spout (Windows only, when enabled)
getImageData() → raw RGBA bytes
prepend "HUFFSPOUT" + width + height header
binary WS to port 8787
Rust relay intercepts on b"HUFFSPOUT"
spout::push_frame() → spoutdx_send_image()
SpoutDX D3D11 UpdateSubresource
The "HUFFSYPH" and "HUFFSPOUT" magic byte prefixes allow the Rust relay to distinguish platform output frames from normal canvas mirror frames on the same WebSocket connection, without requiring a separate port or connection.
- Download
huff-1.0.2-universal.dmgfrom the Releases page. - Open the DMG and drag huff to your Applications folder.
- On first launch, macOS may show a Gatekeeper warning because the app is not notarised. Right-click the app icon → Open → Open to bypass this once.
- Grant camera access when prompted (required for webcam input).
No additional software is needed for Syphon — the framework is bundled inside the app.
- Download
huff-1.0.2-x64-setup.exeor the MSI from the Releases page. - Run the installer. Windows SmartScreen may warn about an unsigned binary — click More info → Run anyway.
- If you plan to use Spout output, install the Spout2 runtime from the Spout website.
- Microsoft Edge WebView2 Runtime is required on Windows 10 (pre-installed on Windows 11).
| Tool | Version | Notes |
|---|---|---|
| Node.js | ≥ 18 LTS | nodejs.org |
| Rust + Cargo | stable | rustup |
| Tauri CLI | 1.x | installed via npm install |
macOS: Xcode Command Line Tools (xcode-select --install)
Windows: Build Tools for Visual Studio with Desktop development with C++. Add the MSVC Rust target: rustup target add x86_64-pc-windows-msvc.
Linux: WebKitGTK development libraries. See Tauri Linux prerequisites.
# Clone and install
git clone https://github.com/your-org/huff.git
cd huff
npm install
# Development (hot-reload)
npm run dev
# Production build
npm run build # macOS: Universal DMG
npx tauri build # Windows/Linux: native installer# Build ARM64 + Intel, lipo them together, package as DMG
bash scripts/create_universal_dmg.sh
# Output: artifacts/huff-universal.dmgREM Builds EXE + NSIS installer + MSI + portable ZIP + SHA256SUMS
scripts\tauri-build.cjs
REM Output: artifacts\<timestamp>\The controls window is divided into collapsible groups. Each group can be collapsed by clicking its label. Groups are independent — collapsing one has no effect on the others or on active effects.
Controls the input material fed into the effect chain.
| Control | Description |
|---|---|
| File | Load a video file. Drag-and-drop also works on the controls window. |
| Camera | Select and start a webcam. Refresh scans for newly connected devices. |
| Play / Pause | Toggle video playback. |
| BASE VIDEO | Show the raw source frame underneath the glitch output. |
| BASE MIX | Opacity of the base video layer (0 = fully glitched, 1 = full base). |
| BASE BG | Background fill colour when no tile covers a pixel: Black, Green (chroma), Blue, White. |
| SEED ON LOAD | Prime the frame ring buffer with the first video frame immediately on file load, rather than waiting for the ring to fill naturally. |
| QUALITY | Scales the frame ring depth (0→4 frames, 1→60 frames) and affects the temporal richness of all time-based effects. Also controls Solarize frame-skip frequency. |
The core datamosh engine. Reads tiles from past frames and blits them onto the current frame at noise-driven positions.
| Control | Range | Description |
|---|---|---|
| ON | toggle | Enable/disable the glitch pass. |
| CORRUPT % | 0–7 | Fraction of grid tiles displaced per frame. 0 = no tiles move. 7 = all tiles replaced. |
| CORRUPT DRIFT | 0–1 | Noise-driven breathing of the corruption density — the percentage oscillates slowly when > 0. |
| PIXEL SIZE | 150–2000 | Size of each grid tile in pixels. Larger = blockier, more visible datamosh. Smaller = fine grain. |
| DEPTH | 0–0.5 | How far back in the frame ring tiles are sampled. 0 = only the most recent frame. 0.5 = up to half the ring depth. |
| DEPTH SCATTER | 0–1 | Per-tile randomisation of the time offset. 0 = all tiles from the same frame. 1 = each tile picks its own depth independently. |
| GLITCH SIZE | 1–60 | Spatial scaling of each tile relative to PIXEL SIZE. |
| OPACITY | 0–1 | Alpha of each blit tile. Lower values ghost the displaced content. |
| JITTER | 0–1 | Magnitude of per-tile position noise. Adds organic displacement on top of the regular grid. |
| SMEAR | 0–200 | Number of duplicate stamps trailed behind each displaced tile. |
| SMEAR ANGLE | 0–360 | Direction of smear trail in degrees. 0 = noise-driven (random per frame). |
| SPEED | 0–10 | Phase velocity of the noise field that drives tile selection and positioning. |
| FINE SPEED | 0–5 | Secondary noise phase — multiplied with SPEED for finer control. |
| MULT | 0–5 | Global speed multiplier applied on top of SPEED × FINE SPEED. |
| GLITCH X / Y | -1–1 | Base offset applied to all tile destination positions. |
| SEED | — | Randomisation seed. Same seed + same parameters = same output. Use to lock a particular glitch pattern. |
Composites the previous output frame back onto itself with a spatial transform. Enables infinite zoom, drift, and rotation effects.
| Control | Range | Description |
|---|---|---|
| FEEDBACK | 0–3 | How strongly the previous frame contributes to the next. 0 = no feedback. Values > 1 over-expose. |
| PERSISTENCE | 0–10 | Decay rate of the feedback buffer between frames. Higher = trails linger longer. |
| FB X | -1–1 | Horizontal translation per frame. Creates lateral drift. |
| FB Y | -1–1 | Vertical translation per frame. Creates vertical drift. |
| FB Z | 0.98–1.03 | Zoom per frame. < 1 = implode, > 1 = explode. |
| FB θ | -2–2 | Rotation in degrees per frame. Creates spinning feedback. |
| Reset Motion | button | Returns FB X, FB Y, FB Z, FB θ to neutral (0, 0, 1.0, 0). |
Displaces horizontal bands of the image by sampling from past frames. Creates a horizontal scan corruption aesthetic.
| Control | Range | Description |
|---|---|---|
| ON | toggle | Enable/disable the scanline pass. |
| BANDS | 0–30 | Number of active displacement bands per frame. |
| BAND HEIGHT | 0–1 | Relative height of each band. |
| RAND SIZE | toggle | Randomise band height per band rather than using a fixed value. |
| SHIFT | 0–0.5 | Maximum horizontal displacement magnitude as a fraction of canvas width. |
| SKEW | -1–1 | Adds a position-based horizontal offset so bands angle across the frame. |
| DRIFT | 0–3 | Vertical drift speed of band positions over time. |
| SPEED | 0–5 | Overall speed multiplier for band drift. |
| GAP | 0–200 | Snaps band positions to a regular grid with this spacing, creating rhythmic rather than random placement. |
| OPACITY | 0–1 | Alpha of displaced band content. |
When enabled, tile placement is biased toward radial clusters rather than uniform scatter. Creates concentrated glitch zones that can drift, spin, and pulse.
| Control | Range | Description |
|---|---|---|
| ON | toggle | Enable cluster-biased tile placement (Glitch must also be ON). |
| CENTERS | 1–20 | Number of cluster centres. |
| SPREAD | 1–300 | Radius around each centre that tiles are distributed within. |
| MIN RAD | 0–150 | Minimum distance from cluster centre — creates a hollow core. |
| SPATIAL GAP | 0–200 | Minimum pixel distance between any two tiles, preventing overlap. |
| BIAS | 0–1 | Fraction of tiles forced into clusters. Remainder are placed randomly. |
| DRIFT | 0–5 | Adds noise-driven movement to cluster centre positions. |
| SPEED | 0–15 | Speed of cluster centre physics simulation. 0 = static positions. |
| INERTIA | 0.01–0.99 | How much cluster centres carry momentum between frames. High inertia = smooth slow movement. Low = jittery. |
Per-pixel luminance-threshold colour inversion. Pixels above the threshold have their channel values inverted by the amount, with independent RGB scaling.
| Control | Range | Description |
|---|---|---|
| ON | toggle | Enable/disable solarize. |
| THRESH | 0–1 | Luminance threshold. Pixels with luma above this are inverted. |
| AMOUNT | 0–1 | Inversion strength. 0 = no effect, 1 = full inversion. |
| SOL R / G / B | 0–2 | Per-channel multiplier applied after inversion. Use to tint the solarised regions. Values > 1 over-expose that channel. |
Distorts UV coordinates using a noise field, creating a liquid or heat-haze displacement.
| Control | Range | Description |
|---|---|---|
| ON | toggle | Enable/disable flow warp. |
| STRENGTH | 0–20 | Maximum pixel displacement applied by the warp. |
| SCALE | 40–200 | Spatial scale of the noise field. Lower = finer warp cells, higher = broader sweeping motion. |
| PULSE | 0–200 | Number of frames back in the ring to sample during warp. Creates temporal smearing as the warped source comes from the past. |
| IMPLODE | 0–1 | Adds a centripetal pull toward the canvas centre on top of the noise warp. |
Mirrors the current frame about one or both axes.
| Control | Options / Range | Description |
|---|---|---|
| SYMM | toggle | Enable/disable symmetry. |
| MODE | V / H / HV | Vertical mirror, horizontal mirror, or both simultaneously. |
| SYM POS | 0–1 | Position of the mirror axis as a fraction of canvas width (V) or height (H). 0.5 = centred. |
Blends multiple past frames as translucent ghost layers underneath the current frame.
| Control | Range | Description |
|---|---|---|
| ON | toggle | Enable/disable trails. |
| TRAIL LAYERS | 0–10 | Number of ghost frames composited. More layers = denser trail. |
| TRAIL DEPTH | 0–1 | How far back in the ring to pull trail frames from. |
| LUMA KEY | 0–1 | Below-threshold luminance pixels in trail frames are dropped. Creates a luma-keyed ghost effect where only bright areas trail. |
Presets save and recall a complete snapshot of all parameter values. They are stored in localStorage and persist across sessions.
- Save — type a name and click Save. If the name already exists it is overwritten.
- Load — select a preset from the dropdown and click Load.
- Delete — select and click Delete.
Factory presets (read-only, always available):
| Preset | Character |
|---|---|
clean |
All effects off — raw source passthrough |
chaos |
High corruption, deep ring, fast clusters |
melt |
Slow feedback zoom with deep scanlines |
mirror |
Symmetry-forward with light solarise |
pulse |
Rhythmic cluster glitch with trails |
solar |
Solarise dominant, flow warp texture |
vapor |
Soft trails, slow feedback rotation |
| Key | Action |
|---|---|
P |
Toggle the controls panel visibility |
F |
Toggle fullscreen on the output canvas window |
huff's MIDI system runs natively in Rust via midir. USB controllers, USB-MIDI interfaces, and virtual ports (IAC Bus on macOS, loopMIDI on Windows) are all supported. MIDI does not go through the browser — there is no Web MIDI API dependency.
- Click the MIDI button in the top bar.
- Click ↻ Refresh to scan for available ports.
- Select your device from the dropdown.
- Click Connect.
The status area confirms the connected port name. To change devices, disconnect first, then select and connect again.
Maps are plain JSON files loaded at runtime. The param field is the DOM element ID of the control you want to drive.
{
"name": "My Controller",
"description": "Optional note",
"version": 1,
"channel": -1,
"mappings": [
{ "param": "feedback", "cc": 14, "type": "range", "enabled": true },
{ "param": "corrupt", "cc": 15, "type": "range", "enabled": true },
{ "param": "corruptOn", "cc": 64, "type": "toggle", "enabled": true },
{ "param": "refreshBtn", "cc": 82, "type": "trigger", "enabled": true }
]
}type values:
range— CC 0–127 is scaled linearly to the parameter's min–maxtoggle— CC > 63 → ON, CC ≤ 63 → OFFtrigger— any CC > 0 fires the button's click event
channel: -1 accepts all channels. 0–15 filters to a specific channel (0-indexed, so channel 1 = 0).
enabled: false disables an entry without deleting it — useful for temporarily silencing a mapping.
| File | Controller |
|---|---|
src/midi/nanokontrol2.json |
Korg nanoKONTROL2 |
src/midi/nanokontrol1.json |
Korg nanoKONTROL (original) |
src/midi/generic.json |
Generic 16-CC template (CC 14–29) |
macOS — IAC Driver:
- Open Audio MIDI Setup (in Applications/Utilities).
- Open the MIDI Studio (⌘2).
- Double-click IAC Driver.
- Enable Device is online, add a port (e.g. "Bus 1").
- The IAC port appears immediately in huff's MIDI port list.
Windows — loopMIDI:
- Download and install loopMIDI.
- Create a virtual port.
- The port appears in huff's MIDI port list.
Once a virtual port exists, Max/MSP, Pure Data, Ableton, and other software can send MIDI to huff through it.
huff listens for UDP OSC on port 9000 on all interfaces (0.0.0.0:9000). Any device on the same local network can send to it.
- Click the OSC button in the top bar.
- The status area confirms the listener is active and shows the port.
- Load a map file via Load Map…. The map persists in
localStorageand reloads automatically on next launch. - The OSC pill in the top bar flashes the address of each received message.
{
"name": "My TouchOSC Layout",
"version": 1,
"mappings": [
{
"param": "feedback",
"addr": "/1/fader1",
"type": "range",
"inputMin": 0,
"inputMax": 1,
"enabled": true,
"note": "Main feedback fader"
},
{
"param": "corruptOn",
"addr": "/1/toggle1",
"type": "toggle",
"enabled": true
}
]
}type values:
range— incoming float mapped frominputMin–inputMaxto the parameter's native min–maxtoggle— value > 0.5 → ON, value ≤ 0.5 → OFFtrigger— value > 0.5 fires the button click
inputMin / inputMax: Set these to match your sender's output range. TouchOSC faders send 0.0–1.0 so use 0, 1. Some MIDI-to-OSC bridges send 0–127 — use 0, 127.
| Software | Method |
|---|---|
| Max/MSP | [udpsend 127.0.0.1 9000] |
| Pure Data | [netsend -u -b 127.0.0.1 9000] |
| SuperCollider | NetAddr("127.0.0.1", 9000).sendMsg("/huff/feedback", 0.75) |
| TouchDesigner | OSC Out DAT, host 127.0.0.1, port 9000 |
| Python (pythonosc) | SimpleUDPClient("127.0.0.1", 9000) |
| Protokol / OSCQuery | Any app supporting OSC output |
- In TouchOSC go to Settings → OSC.
- Set Host to your computer's local IP (e.g.
192.168.1.42). - Set Port (outgoing) to
9000. - Enable OSC.
- Load one of the bundled map files (
src/osc/touchosc-effects.jsonortouchosc-mix.json) in huff's OSC panel.
Bundled maps reference the standard TouchOSC /1/faderN and /1/toggleN address scheme used by the built-in Simple layout.
Syphon lets huff share its canvas as a named texture that any Syphon-enabled application can receive in real time — without capture cards, screen recording, or NDI.
Compatible receivers: Resolume Avenue/Arena, VDMX, MadMapper, CoGe, Millumin, Modul8, Processing (Syphon library), Max/MSP (Jitter), and anything that supports the Syphon protocol.
Usage:
- Click the SYPHON button in the top bar.
- Set the output resolution (default: 1280×720).
- Set the FPS cap (default: 30).
- Click ▶ Start. The sender appears as
huffin any Syphon receiver. - Click ■ Stop to close the server.
Technical notes:
- The Syphon.framework is bundled inside the huff app bundle — no separate installation is needed.
- The pipeline is:
canvas.getImageData()→ raw RGBA bytes →HUFFSYPHbinary WS frame → Rustsyphon::push_frame()→MTLTextureCPU upload →SyphonMetalServer.publishFrameTexture(). - This uses a CPU round-trip (JS pixel readback). Frame rate is throttled to the configured FPS cap to limit the readback cost.
Spout is the Windows equivalent of Syphon. huff shares its canvas as a named D3D11 texture that any Spout2-enabled application can receive.
Compatible receivers: Resolume Arena, TouchDesigner, MadMapper, Notch, and anything that supports the Spout2 protocol.
Usage:
- Install the Spout2 runtime if not already installed.
- Click the SPOUT button in the top bar.
- Set the output resolution and FPS cap.
- Click ▶ Start. The sender appears as
huffin Spout receivers.
Technical notes:
- The Spout bridge DLL (
spout_bridge.dll) is compiled from source and bundled next to the huff executable. - The pipeline is:
canvas.getImageData()→ raw RGBA bytes →HUFFSPOUTbinary WS frame → Rustspout::push_frame()→spoutdx_send_image()→ D3D11UpdateSubresource. - Same CPU round-trip as Syphon. GPU-direct zero-copy is not implemented in this release.
The separate output canvas window (canvas.html) connects to the embedded WebSocket relay on port 8787 and receives JPEG-encoded frames. This window can be:
- Moved to a second monitor and made fullscreen (
Fkey) - Opened in a browser by navigating to
file:///path/to/huff/src/canvas.htmlwhile the app is running - Used as an OBS window-capture source
The canvas window does not apply any additional effects — it displays exactly what the controls window output is.
Complete list of all mappable parameter IDs, ranges, and descriptions for use in MIDI and OSC map files.
| ID | Label | Min | Max |
|---|---|---|---|
baseMix |
Base Mix | 0 | 1 |
quality |
Quality | 0 | 3 |
feedback |
Feedback | 0 | 3 |
persistence |
Persistence | 0 | 10 |
fbX |
FB X | -1 | 1 |
fbY |
FB Y | -1 | 1 |
fbZ |
FB Z | 0.98 | 1.03 |
fbTheta |
FB θ | -2 | 2 |
depth |
Depth | 0 | 0.5 |
depthScatter |
Scatter | 0 | 1 |
corrupt |
Corrupt % | 0 | 7 |
corruptDrift |
Corrupt Drift | 0 | 1 |
block |
Pixel Size | 150 | 2000 |
glitchSize |
Glitch Size | 1 | 60 |
glitchAlpha |
Tile Opacity | 0 | 1 |
glitchJitter |
Jitter | 0 | 1 |
glitchSmear |
Smear | 0 | 200 |
glitchSmearAngle |
Smear Angle | 0 | 360 |
glitchSpeed |
Glitch Speed | 0 | 10 |
glitchSpeedFine |
Fine Speed | 0 | 5 |
glitchSpeedMul |
Speed Mult | 0 | 5 |
glitchBaseX |
Glitch X | -1 | 1 |
glitchBaseY |
Glitch Y | -1 | 1 |
trailLayers |
Trail Layers | 0 | 10 |
trailDepth |
Trail Depth | 0 | 1 |
trailLumaKey |
Luma Key | 0 | 1 |
symPos |
Sym Position | 0 | 1 |
scanShift |
Scan Shift | 0 | 0.5 |
scanDrift |
Scan Drift | 0 | 3 |
scanSpeed |
Scan Speed | 0 | 5 |
scanGap |
Scan Gap | 0 | 200 |
scanSkew |
Scan Skew | -1 | 1 |
scanAlpha |
Scan Opacity | 0 | 1 |
clusterCount |
Scan Bands | 0 | 30 |
clusterRadius |
Band Height | 0 | 1 |
cluCenters |
Clu Centers | 1 | 20 |
cluSpread |
Clu Spread | 1 | 300 |
cluMinSpread |
Clu Min Rad | 0 | 150 |
spatialGap |
Spatial Gap | 0 | 200 |
cluBias |
Clu Bias | 0 | 1 |
cluDrift |
Clu Drift | 0 | 5 |
cluSpeed |
Clu Speed | 0 | 15 |
cluInertia |
Clu Inertia | 0.01 | 0.99 |
solarizeThresh |
Sol Thresh | 0 | 1 |
solarizeAmt |
Sol Amount | 0 | 1 |
solarizeR |
Sol R | 0 | 2 |
solarizeG |
Sol G | 0 | 2 |
solarizeB |
Sol B | 0 | 2 |
flowStrength |
Flow Strength | 0 | 20 |
flowScale |
Flow Scale | 40 | 200 |
flowPulse |
Flow Pulse | 0 | 200 |
flowImpl |
Flow Implode | 0 | 1 |
| ID | Label |
|---|---|
corruptOn |
Glitch On |
baseOn |
Base Video |
seedOnLoad |
Seed on Load |
symOn |
Symmetry On |
clusters |
Scanlines On |
scanRandSize |
Rand Band Size |
clusterTiles |
Cluster Tiles On |
solarizeOn |
Solarize On |
flowOn |
Flow Warp On |
trailOn |
Trails On |
| ID | Action |
|---|---|
refreshBtn |
Re-seed glitch randomisation |
resetMotionBtn |
Reset FB X/Y/Z/θ to defaults |
resetBtn |
Reset all parameters to defaults |
huff/
├── src/ # Frontend (HTML + JS, no bundler)
│ ├── index.html # Controls window — all UI, p5 draw loop, effect chain
│ ├── canvas.html # Output mirror window — receives WS frames
│ ├── canvas.js # p5 setup, draw loop, video handling, WS sender
│ ├── effects.js # applyGlitch, applyTrails, applyScanlines,
│ │ # applyFlowWarp, applySolarize, applySymmetry
│ ├── p5.js # p5.js library (bundled locally, no CDN)
│ ├── midi/ # Bundled MIDI map files
│ ├── osc/ # Bundled OSC map files
│ └── presets/ # Factory preset JSON files
│
├── src-tauri/
│ ├── src/
│ │ ├── main.rs # Rust entry: WS relay, MIDI commands, OSC listener,
│ │ │ # Syphon + Spout command handlers
│ │ ├── syphon.rs # macOS Syphon ObjC FFI (Metal texture upload)
│ │ └── spout.rs # Windows Spout2 D3D11 bridge
│ ├── native/
│ │ └── spout_bridge/ # C++ SpoutDX bridge (compiled by build.rs via CMake)
│ │ ├── spout_bridge.cpp
│ │ ├── spout_bridge.h
│ │ └── CMakeLists.txt
│ ├── frameworks/
│ │ └── Syphon.framework # Bundled macOS Syphon framework
│ ├── Cargo.toml
│ ├── build.rs # CMake invocation for Spout (Windows only)
│ ├── tauri.conf.json
│ └── capabilities/
│ └── network.json # Tauri capability: WS relay access
│
├── scripts/
│ ├── create_universal_dmg.sh # macOS: lipo + DMG packaging
│ └── tauri-build.cjs # Windows: EXE + MSI + ZIP + checksums
│
└── package.json
WebSocket relay runs inside the Tauri Rust process. It binds on 127.0.0.1:8787 (IPv4) and [::1]:8787 (IPv6). Clients register as index (sender) or canvas (receiver) via a JSON handshake. Binary frames are forwarded only to canvas clients unless they carry a HUFFSYPH or HUFFSPOUT magic prefix, in which case Rust intercepts and routes them to the platform output subsystem.
Rust crates used:
| Crate | Purpose |
|---|---|
tauri 1.x |
App shell, two-window management, IPC |
tokio 1.x |
Async runtime for WS relay and OSC listener |
tokio-tungstenite 0.21 |
WebSocket server |
midir 0.9 |
Native MIDI input |
rosc 0.10 |
OSC UDP packet decoder |
objc / objc-foundation |
macOS Syphon ObjC FFI (macOS only) |
cmake 0.1 |
Spout bridge CMake build (Windows only) |
- Frame ring stores
ImageDataobjects (raw RGBA pixel arrays) rather than p5Graphicsinstances. This avoids theget()copy-on-read allocation and keeps peak memory predictable. - 192 MB ring cap — the ring depth is capped regardless of the quality setting. At 1080p (≈8 MB per frame) this gives roughly 24 frames maximum. At 720p (≈3.7 MB) you get the full 60 frames at quality=1.
drawRingRegionuses a single shared offscreen<canvas>withputImageData+ nativedrawImagecropping per tile. No per-tile allocation.- Solarize downsamples to a 640px-wide scratch canvas before the pixel pass, then scales back up. This is 4–16× faster on large canvases and makes a large practical difference on Windows/DirectX WebView.
- Flow warp renders in tiles rather than per-pixel — the tile size is set by the SCALE parameter. Larger tiles = faster but coarser warp.
- QUALITY slider controls ring depth and also determines how often Solarize runs (it skips frames proportionally at lower quality values).
- Feedback uses
drawingContext.drawImagedirectly rather thanp5.get(), eliminating one full-canvas copy per frame. - If the app stutters, try: lower QUALITY → reduce canvas resolution → increase PIXEL SIZE → disable Flow Warp (the most expensive pass).
CPU pixel readback for Syphon and Spout.
Both output routes use getImageData() to read pixels from the canvas back to the CPU, then send them over the local WebSocket to Rust, which uploads them to a GPU texture. This is a full GPU→CPU→GPU round trip per frame. It works well at 720p/30fps but is not zero-copy. GPU-direct sharing (sharing the WebGL texture handle directly with Syphon/Spout) is not feasible in the Tauri WebView context in this release.
Frame rate is throttled for Syphon/Spout. The FPS cap in the Syphon/Spout panels defaults to 30 fps and should not be set higher than your actual canvas frame rate. Sending faster than the canvas draws produces duplicate frames and wastes CPU.
Memory grows with resolution. The 192 MB ring cap is enforced by frame count, not pixel size. At 4K resolution, the ring effectively holds only a few frames regardless of the quality setting, and the datamosh effect loses temporal depth. 720p or 1080p is the practical sweet spot.
Syphon is macOS-only. Spout is Windows-only. These are platform protocols with no cross-platform equivalent in this release. If you need cross-platform texture sharing, use OBS window capture or NDI (not currently implemented).
Camera permissions require codesigning for distribution.
In development (npm run dev) the camera entitlement is injected by Tauri automatically. In distribution builds on macOS, the app must be codesigned with the NSCameraUsageDescription entitlement for the permission dialog to appear. Unsigned builds will silently fail the camera permission request on macOS 12+.
SmartScreen warning on Windows. Unsigned Windows builds trigger a SmartScreen warning. This is expected — click More info → Run anyway. For signed distribution, sign the EXE with a code-signing certificate before publishing.
Port 8787 must be free.
The WS relay binds to 127.0.0.1:8787 and [::1]:8787 at startup. If another application is already using this port, the relay will fail silently and the canvas mirror window will not receive frames. Change const PORT: u16 = 8787 in src-tauri/src/main.rs and update the __getWSURL__ call in src/index.html to match.
Linux is untested. The Tauri scaffolding supports Linux and the build instructions include Linux prerequisites, but no binary has been tested against a Linux runtime in this release. Syphon and Spout are not available on Linux. Community contributions welcome.
OSC map persists in localStorage per-origin.
The OSC map is stored in the WebView's localStorage. If you clear browser data for the app origin or uninstall and reinstall, the map will be lost. Export maps as JSON files and keep them alongside your project files.
No multi-instance support. Running two instances of huff simultaneously will cause a port conflict on 8787. If you need parallel instances, build separate copies with different port constants.
Blank canvas window / "WS: disconnected"
The canvas window connects to the relay in the Tauri process. If you opened canvas.html directly in a browser outside of the app, run node src/ws-server.js in the project directory to start a standalone relay.
Camera not appearing Click ↻ Refresh in the Source group after connecting your camera. On macOS, the first time the app requests camera access a system dialog appears — grant access and refresh again. If no dialog appears and the camera still doesn't show, the app may not be codesigned correctly for the permission to be requested.
MIDI device not appearing Click ↻ Refresh in the MIDI panel. The port list is read fresh on each refresh — devices plugged in after the panel was opened do not appear automatically.
Syphon not visible to receivers Confirm the Syphon server shows active status in the Syphon panel. In your receiver (e.g. Resolume), trigger a re-scan of Syphon sources. If the app was just launched, wait 1–2 seconds before scanning — the server registers asynchronously.
Spout init failed
Install the Spout2 runtime. The spoutdx_send_image function in the bridge DLL requires the Spout2 runtime DLLs to be present on the system.
High memory usage Lower the QUALITY slider. At quality=1 and 1080p the ring can hold up to 192 MB. At quality=0 it holds only 4 frames.
Port 8787 already in use Find which process is using the port:
# macOS/Linux
lsof -i :8787
# Windows
netstat -ano | findstr :8787Change PORT in src-tauri/src/main.rs and WS_MIRROR_URL in src/index.html to an unused port, then rebuild.
tauri build fails on Linux with missing library
Re-run the WebKitGTK/libssl dependency install for your distribution from the Building from Source section.
huff v1.0.2 beta · built with Tauri, p5.js, Rust, Syphon, Spout2 · ISC licence