Releases: schwwaaa/huff
🚀 huff --- beta-v1.0.7
Changelog
[1.0.7] — Beta Release
First public beta. Confirmed building and running on macOS (Apple Silicon + Intel) and Windows (x64).
What is huff
A real-time datamosh and glitch-art desktop application built with Tauri v1 + p5.js. Load a video file or connect a webcam, then sculpt live datamoshing, feedback loops, flow warps, symmetry folds, solarise, scanline corruption, and ghost trails — all streamed to a separate fullscreen output window over an embedded WebSocket relay.
Added
Core effect engine
- Datamosh / glitch tile displacement — temporal tile sampling from a 60-frame ring buffer with configurable depth, scatter, and cluster geometry
- Feedback loop — per-frame zoom, translate, and rotate composited back onto the buffer
- Flow warp — noise-field optical-flow UV distortion with pulse and implosion modes
- Symmetry — vertical, horizontal, or both axes with adjustable position
- Solarise — luminance-threshold colour inversion with per-channel R/G/B tinting
- Scanline bands — drifting horizontal displacement bands sampled from past frames
- Trail accordion — stacked ghost frames for motion-smear effects
- Smear — directional tile stamping with noise-driven or fixed angle
Input
- Camera/webcam live feed via
getUserMedia - Video file loading with drag-and-drop
- Seed buffer on load — primes the frame ring with the first video frame
Output
- Embedded WebSocket relay (port 8787, IPv4 + IPv6) — binary JPEG frames forwarded only to
canvasrole clients - Two-window architecture — controls in
index.html, fullscreen output mirror incanvas.html - Syphon output (macOS) — Metal texture upload via
SyphonMetalServerObjC FFI; activated from the controls panel; visible ashuffto any Syphon client (Resolume, VDMX, MadMapper, CoGe, etc.) - Spout output (Windows) — SpoutDX D3D11 pixel upload via
spoutdx_send_imageC-ABI bridge; activated from the controls panel; visible ashuffto any Spout2 receiver (Resolume Arena, TouchDesigner, VDMX, etc.)
MIDI
- Full MIDI input via
midir— note on/off, CC, pitch bend, aftertouch, program change - Connect by port name or index via Tauri commands (
connect_midi_port_by_name,list_midi_ports) - MIDI events emitted to the frontend as
midi-eventTauri events - Bundled mapping files for Korg nanoKONTROL1, nanoKONTROL2, and a generic 16-control layout
OSC
- UDP OSC listener on port 9000 via
rosc - Messages emitted to the frontend as
osc-messageTauri events with normalised float values - Bundled TouchOSC layout files for effects and mixer control
Presets
- 7 factory presets:
clean,chaos,melt,mirror,pulse,solar,vapor - JSON preset format documented in
src/midi/FORMAT.mdandsrc/osc/FORMAT.md
Build system
- macOS: Universal binary (ARM64 + x64) via
lipo, packaged as DMG —scripts/create_universal_dmg.sh - Windows: EXE + NSIS installer + MSI + portable ZIP with SHA256 checksums —
scripts/tauri-build.cjs - Spout bridge compiled at build time via CMake from
src-tauri/native/spout_bridge/against the bundled Spout2 SDK
Platform notes
macOS
- Syphon.framework bundled in
src-tauri/frameworks/and declared intauri.conf.jsonbundle.macOS.frameworks— no separate install required - Camera entitlement present in
src-tauri/entitlements.plist; works in dev mode without codesigning - Universal DMG tested on Apple Silicon (M-series) and Intel
Windows
- Spout requires the Spout2 runtime installed on the target machine
- WebView2 runtime required on Windows < 11
- SmartScreen warning expected on unsigned builds — click More info → Run anyway
spout_bridge.dllcopied next to the executable automatically at build time
Known limitations
- Syphon uses a CPU round-trip:
getImageData()in JS → binary WS frame → Metal texture upload in Rust. Frame rate is throttled to 30 fps on the JS side to limit readback cost. - Spout uses the same CPU path via
spoutdx_send_image. GPU-direct zero-copy is not implemented in this release. - Frame ring memory is capped at 256 MB regardless of quality setting; at 4K resolution the effective ring depth is reduced.
- Linux builds are untested in this release — the Tauri scaffolding supports it but no binary has been verified.
Dependencies
| Crate | Version | Purpose |
|---|---|---|
| tauri | 1.x | App shell, WebView, two-window management |
| tokio | 1.x | Async runtime |
| tokio-tungstenite | 0.21 | Embedded WebSocket relay |
| midir | 0.9 | MIDI input |
| rosc | 0.10 | OSC UDP listener |
| objc / objc-foundation | 0.2 / 0.1 | macOS Syphon ObjC FFI (macOS only) |
| cmake | 0.1 | Spout bridge build (Windows only) |
Frontend: p5.js (bundled locally, no CDN dependency).
How to build from source
# Install dependencies
npm install
# Development (hot-reload)
npm run dev
# Production
npm run build # macOS: Universal DMG
npx tauri build # Windows/Linux: platform native installerFull build instructions in README.md.
🚀 huff --- beta-v1.0.6
Changelog
Real-time datamosh / glitch-art desktop application · Tauri + p5.js
⏺ Recording
huff can now capture its output directly to disk. A native OS Save dialog opens when you stop — no hidden app folders, no guessing where the file went.
| Mode | Output | Requires |
|---|---|---|
| MP4 | H.264 video, web-optimised | Bundled FFmpeg sidecar |
| MOV | H.264 QuickTime container | Bundled FFmpeg sidecar |
| ZIP | All frames packed as JPEGs | Nothing extra |
| Image sequence | Numbered JPEGs in a timestamped folder | Nothing extra |
- Full canvas resolution — recordings capture at your actual window size, not the 1280px-capped mirror stream
- Snapshot — grab a single frame as a JPEG at any time without starting a full recording
- 24 / 30 / 60 fps — selectable before encoding
- Live frame counter — see exactly how many frames are buffered while recording
- Graceful cancel — closing the Save dialog discards frames cleanly with no error
The ⏺ REC button in the toolbar opens the recording panel. Click it again while actively recording to stop and trigger the Save dialog immediately.
FFmpeg (MP4 / MOV)
MP4 and MOV encoding uses a bundled FFmpeg binary shipped inside the app — no system install required on end-user machines.
Run the setup script once before building:
# macOS
./scripts/download-ffmpeg.sh
# Windows
scripts\download-ffmpeg.cmdBinaries are sourced from evermeet.cx (macOS) and gyan.dev (Windows) — minimal static builds with libx264. Bundled at build time via Tauri's externalBin sidecar mechanism and gitignored from the repo.
What changed since beta-v1.0.5
- feat: Recording engine — MP4, MOV, ZIP, image sequence output via native Save dialog
- feat: Bundled FFmpeg sidecar — H.264 encoding with no system dependency
- feat: Full-resolution frame capture — bypasses the 1280px WS mirror stream cap
- feat: Snapshot command — single-frame JPEG capture at any time
- feat:
⏺ RECbutton and status pill in the control bar with recording blink indicator - feat: FPS selector (24 / 30 / 60) for video output
- fix: Effect chain independence — trails, scanlines, flow warp, symmetry, solarise, and feedback all work without the glitch engine active
- chore:
zipcrate added for archive output - chore: Tauri
dialogfeature flag enabled for native Save / Pick-folder dialogs - chore:
scripts/download-ffmpeg.sh+scripts/download-ffmpeg.cmdfor FFmpeg binary setup
Full changelog by version
| Tag | Highlight |
|---|---|
beta-v1.0.1 |
Universal macOS + Windows build pipeline |
beta-v1.0.2 |
Stable core — glitch engine, feedback, flow, trails, solarise |
beta-v1.0.3 |
MIDI input — nanoKONTROL2 + generic CC mapping |
beta-v1.0.4 |
OSC input — TouchOSC, Max/MSP, Pure Data, SuperCollider |
beta-v1.0.5 |
Syphon output (macOS) — send live frames to VJ software |
beta-v1.0.6 |
Recording — MP4, MOV, ZIP, image sequence |
Platform downloads
| Platform | File |
|---|---|
| macOS Universal (Apple Silicon + Intel) | huff_beta-v1.0.6_universal.dmg |
| Windows x64 NSIS installer | huff_beta-v1.0.6_x64-setup.exe |
| Windows x64 portable | huff_beta-v1.0.6_x64.exe |
| Windows x64 MSI | huff_beta-v1.0.6_x64_en-US.msi |
| Linux x64 AppImage | huff_beta-v1.0.6_amd64.AppImage |
| Linux x64 deb | huff_beta-v1.0.6_amd64.deb |
Known
- FFmpeg encoding of very long recordings may take a moment after the Save dialog closes
- Syphon output and recording can run simultaneously without conflict
- SmartScreen warning on unsigned Windows builds is expected — click More info → Run anyway
- Camera permission dialog on macOS requires a codesigned build for distribution; dev mode (
npm run dev) works without signing
🚀 huff --- beta-v1.0.5
Changelog
All notable changes to huff are documented here.
Format follows Keep a Changelog.
[1.0.5] — 2026-03-09 — Syphon Output
Overview
v1.0.5 adds real-time Syphon output — huff's rendered canvas can now be shared with any Syphon-compatible application (Resolume, VDMX, MadMapper, CoGe, OBS, Millumin) at a configurable resolution and frame rate. Syphon.framework is bundled as a universal binary inside the project; no system installation is required. The universal .app + .dmg build pipeline is also stabilised in this release via a custom build.sh that handles the fat-framework lipo problem.
Added — Syphon output (macOS)
src-tauri/src/syphon.rs — new module, macOS only (#[cfg(target_os = "macos")])
ensure_framework_loaded()— loadsSyphon.frameworkat runtime viaNSBundle. Checks four paths in order: bundled.app(Contents/Frameworks/), dev build source (src-tauri/frameworks/),~/Library/Frameworks,/Library/Frameworks. Never linked at compile time — the binary runs on non-Syphon systems with a clean diagnostic error.start(width, height)— creates aMTLDevice(system default GPU),MTLCommandQueue, andSyphonMetalServernamed"huff". Drops and replaces any existing server on repeat calls.stop()— calls[server stop]and releases all Metal state.push_frame(data: &[u8])— called per frame from the WS relay. Parses the 16-byteHUFFSYPHheader, creates aMTLTexturewith shared storage mode (zero-copy on Apple Silicon), uploads RGBA pixels viareplaceRegion:mipmapLevel:withBytes:bytesPerRow:, then publishes viapublishFrameTexture:onCommandBuffer:imageRegion:flipped:YES.flipped: YEScorrects Canvas 2D top-left origin to Metal/Syphon bottom-left origin. Per-frame texture is released immediately after publish.FRAME_COUNT: AtomicU64— incremented on every successful publish, exposed viastatus().is_active() → bool,status() → String— query helpers for the frontend.
Binary frame protocol (HUFFSYPH):
Bytes 0– 7 b"HUFFSYPH" magic identifier
Bytes 8–11 width u32 little-endian
Bytes 12–15 height u32 little-endian
Bytes 16+ pixels raw RGBA8, width × height × 4 bytes
src-tauri/src/main.rs
#[macro_use] extern crate objc— added,#[cfg(target_os = "macos")]gated. Required becausemsg_send!expands internally tosel!calls that must be in crate scope; auseimport in the child module is insufficient due to howmacro_rules!resolution works.- Binary WS handler — added
HUFFSYPHintercept: frames with the magic prefix are routed tosyphon::push_frame()and are not relayed to the canvas window (avoids broadcasting multi-megabyte pixel payloads to a client that doesn't need them). start_syphon({ width, height }),stop_syphon(),syphon_status()Tauri commands registered.- Non-macOS stubs return
Err("Syphon is macOS only")— the app compiles and runs on Windows/Linux unchanged.
src-tauri/Cargo.toml
objc,objc-foundation,objc_idmoved exclusively to[target.'cfg(target_os = "macos")'.dependencies]— removes the duplicate optional/non-optional conflict that caused earlier build failures.
src-tauri/tauri.conf.json
"frameworks": ["frameworks/Syphon.framework"]added totauri.macOSbundle block. Path is relative tosrc-tauri/. Tauri copies the framework toContents/Frameworks/at build time.
src-tauri/frameworks/Syphon.framework — bundled
- Universal binary (x86_64 + arm64), full
Versions/Astructure with_CodeSignature. - Includes
default.metallib(Metal shader library required bySyphonServerRendererMetal).
src/index.html — Syphon engine and UI
- SYPHON pill (purple) in topbar — shows
SYPHON: OFF/SYPHON: W×H; click opens modal. - Syphon modal — purple-accented:
- Resolution inputs (default 1280×720), independent of canvas window size.
- FPS cap selector (60 / 30 / 24 / 15).
- Start / Stop button with status box and frame counter (polled via
syphon_status()every 2s). - Setup notes for OBS, Resolume, VDMX, MadMapper.
- Syphon engine (IIFE):
- Dedicated WebSocket connection as role
"syphon-sender"— separate from canvas-mirror connection. drawImage(canvas, 0, 0, targetW, targetH)scales to target resolution beforegetImageData(), so the Syphon output is always exactly the specified dimensions regardless of canvas window size.- rAF loop respects FPS cap via timestamp delta.
- Dedicated WebSocket connection as role
Fixed — Syphon output resolution (letterboxing in OBS)
The initial implementation captured pixels at native p5 canvas size. If the canvas window was narrower than the configured Syphon resolution, clients padded with black bars. Fixed by scaling via drawImage() to targetW × targetH before the getImageData() call — the browser compositing pipeline performs the scale GPU-side at zero cost, and the frame sent to Rust is always exactly the right size.
Added — Universal build pipeline
build.sh — new file, replaces direct npx tauri build for distribution builds
build_mac_arm— compilesaarch64-apple-darwin, auto-installs Rust target if missing.build_mac_x86— compilesx86_64-apple-darwin.build_mac_universal— runs both arch builds, then assembles a universal.appand.dmgmanually:thin_bundle_frameworks(app, arch)— strips every fat Mach-O inContents/Frameworks/to a single arch usinglipo -thinbefore the combine step. This solves the Tauri universal bundler limitation: Tauri's lipo pass cannot merge two fat binaries with overlapping architectures (theSyphon.frameworkbinary is already universal). By thinning each arch bundle first, the subsequentlipo -createsucceeds cleanly.lipo -createpass — walks all Mach-O files in the ARM bundle and merges with x86_64 counterparts.- Optional
create-dmgstep — produces a distributable.dmgifcreate-dmgis installed (brew install create-dmg).
build_windows— Windows x86_64 via native MSVC orcargo-xwin/crossfor cross-compilation from macOS.- Written for bash 3.2 (macOS system shell) — no
declare -Aor other bash 4 features.
./build.sh # auto-detect OS
./build.sh mac-universal # → target/universal/…/huff-universal.dmg
./build.sh mac-arm # Apple Silicon only
./build.sh mac-x86 # Intel only
./build.sh windows # Windows x86_64
./build.sh all # all targets
[1.0.4] — 2026-03-09 — OSC
Added — OSC (Open Sound Control)
rosc = "0.10"— pure-Rust OSC decoder, no system dependencies- UDP listener on
0.0.0.0:9000— started at app launch, handles bare messages and nested bundles recursively OscEvent { addr, value, args }— emitted to all windows viaapp.emit_all("osc-message")get_osc_port()Tauri command- OSC modal (green-accented) — status box, map load/clear, mappings table, TouchOSC setup guide
- OSC engine (IIFE) —
addrLookup,applyOsc(),flashCtrl(),localStoragemap persistence - Factory maps:
touchosc-mix.json,touchosc-effects.json,generic-16.json,osc-validate.json src/osc/FORMAT.md— full authoring reference
[1.0.3] — 2026-03-09 — Native MIDI
Added — MIDI via Rust/midir
midir = "0.9"— CoreMIDI / ALSA / WinMM, no Node.js sidecarMidiEvent { kind, channel, data1, data2, value, raw }— emitted as"midi-event"list_midi_ports(),connect_midi_port_by_name(),connect_midi_port(),disconnect_midi(),debug_midi_ports()Tauri commands"withGlobalTauri": trueadded totauri.conf.json— required forwindow.__TAURI__in WebView- MIDI modal (amber-accented) — port selector, connect/disconnect/debug, map load/clear, auto-reconnect on launch
- MIDI engine (IIFE) —
ccLookup,applyCC(),flashCtrl(),localStoragemap persistence - Factory maps:
nanokontrol2.json,nanokontrol1.json,generic.json src/midi/FORMAT.md— full authoring reference
Removed
src/midi-bridge.cjs— Node.js WebSocket sidecar (superseded)easymidi,wsnpm dependenciesnpm run midiscript
[1.0.2] — 2026-03-08 — Scanlines, Cluster Physics, UI
Added
scanSpeed,scanGap,scanSkew— scanline animation parameterscluMinSpread,cluBias,cluDrift— cluster geometry parameterscluSpeed,cluInertia— cluster physics simulation (velocity integration with Perlin steering;cluSpeed === 0preserves legacy static mode at zero cost)- Module-level
_cluPhysics[],_cluPhysT— physics state persists across frames
Changed
- PRESETS group moved above SOURCE in the control panel
- MOTION group merged into GLITCH
- ↺ Reset Motion button added to Feedback group
[1.0.1] — prior
Initial internal release. Core graphics pipeline, two-window Tauri architecture, WebSocket relay, preset system.
Dependency summary
| Package | Version | Layer | Purpose |
|---|---|---|---|
tauri |
1.x | Rust | Desktop shell, IPC, window management |
midir |
0.9 | Rust | MIDI — CoreMIDI / ALSA / WinMM |
rosc |
0.10 | Rust | OSC packet decoding |
tokio |
1.x | Rust | Async runtime — WS relay + OSC UDP |
tokio-tungstenite |
0.21 | Rust | WebSocket server |
serde / serde_json |
1.x | Rust | JSON serialisation |
once_cell |
1.x | Rust | Static initialisation |
objc |
0.2 | Rust | ObjC FFI — macOS, Syphon/Metal interop |
p5.js |
1.11.0 | JS | Canvas 2D rendering |
@tauri-apps/cli |
1.x | JS | Build tooling |
Syphon.framework |
bundled | macOS | IOSurface Metal texture sharing |
🚀 huff --- beta-v1.0.4 - MIDI + OSC
Changelog
Added — OSC (Open Sound Control)
Rust (src-tauri/src/main.rs)
- Added
rosc = "0.10"toCargo.toml— pure-Rust OSC decoder, no system dependencies run_osc_listener()— async Tokio task that binds0.0.0.0:9000at app startup, receives UDP packets, and decodes them viarosc::decoder::decode_udp()dispatch_osc()— recursively handles both bareOscPacket::MessageandOscPacket::Bundle(bundles containing multiple messages are fully unwrapped)OscEventstruct — serialised and emitted to all Tauri windows as"osc-message"events:{ addr, value, args }value— first numeric arg (float as-is; int ÷ 127 to normalise)args— all arguments serialised as strings for the monitor display
get_osc_port()Tauri command — returns9000, lets the frontend display the active port without hardcoding it in JSOSC_SHUTDOWNstaticOnceCell— holds aoneshot::Senderto cleanly stop the listener if needed- Listener is spawned inside
.setup()alongside the existing WebSocket relay; theAppHandleis passed in soemit_all()reaches all windows
Frontend (src/index.html)
- OSC button added to topbar — opens the OSC modal
- OSC pill added to topbar — shows
:9000when listener is active; flashes the incoming OSC address on every message for 700ms - OSC modal (
#oscOverlay) — green-accented, mirrors the MIDI modal layout:- Status box — confirms listener is live and shows the UDP port
- Load Map / Clear Map buttons with file picker (
.json) - Active Mappings table — address, parameter ID, type, input range, note columns;
_sectionkeys render as divider rows - Setup instructions for TouchOSC, Max/MSP, Pure Data, and same-machine soft OSC
- OSC engine (IIFE at bottom of
<body>):buildLookup(map)— buildsaddrLookup: Map<addr, [{paramId, type, inputMin, inputMax}]>applyOsc(addr, rawValue)— normalisesinputMin–inputMaxto 0–1, then scales to elementmin–max; dispatchesinputevent for ranges,changefor toggles,clickfor triggersflashCtrl(paramId)— 120ms green.osc-activeborder flash on the target.ctrlchip- Map persists to
localStoragekeyhuffOscMapand reloads on next launch - Modal open/close wired unconditionally before any Tauri guard so the button always responds
OSC map files (src/osc/)
FORMAT.md— complete authoring reference: schema, all three types, full parameter table with min/max, soft OSC examples for Max/MSP, Pure Data, SuperCollider, TouchDesigner, Pythontouchosc-mix.json— 8 faders + 6 toggles + 2 triggers; designed for the TouchOSC built-in Mix template (page/1/)touchosc-effects.json— 16 rotary knobs + 4 faders; covers glitch geometry, cluster physics, scanlines, symmetry/solarize; designed for a 4×4 grid layout (page/2/)generic-16.json— 16/huff/namespaced addresses; works with any OSC sender without template dependenciesosc-validate.json— 3-address smoke test: one range, one toggle, one trigger
[1.0.3] — 2026-03-09
Added — MIDI (native via Rust/midir)
Rust (src-tauri/src/main.rs)
- Added
midir = "0.9"toCargo.toml— cross-platform MIDI I/O (CoreMIDI / ALSA / WinMM) MidiEventstruct — serialised and emitted as"midi-event":{ kind, channel, data1, data2, value, raw }kind—cc,note_on,note_off,pitch_bend,aftertouch,program_change,pressure,unknownvalue—data2 / 127.0, normalised 0.0–1.0
parse_midi()— decodes raw MIDI bytes intoMidiEvent; handles 0x90 velocity-0 as note-off; normalises 14-bit pitch bend to 0–127MIDI_CONN—Lazy<Mutex<Option<MidiInputConnection>>>global that keeps the connection alive between Tauri commandslist_midi_ports()— creates a freshMidiInputon each call so virtual ports (IAC Bus, loopMIDI, Max/MSP, Pure Data) created after launch appear in the list without restartconnect_midi_port_by_name({ portName })— exact match first, then case-insensitive substring fallback; drops any existing connection before opening the new oneconnect_midi_port({ portIndex })— legacy index-based connect retained for compatibilitydisconnect_midi()— drops theMidiInputConnection, releasing the port back to the OSdebug_midi_ports()— enumerates all visible ports to stdout; returns the same string to the caller for display in the UI
tauri.conf.json
- Added
"withGlobalTauri": trueto thebuildblock — required forwindow.__TAURI__to be injected into the WebView in Tauri v1. Without this flag allinvoke()andlisten()calls silently fail.
Frontend (src/index.html)
- MIDI button added to topbar
- MIDI pill added to topbar — shows connected port name; flashes
CC N=Von every incoming CC message for 600ms - MIDI modal (
#midiOverlay) — amber-accented:- Port dropdown populated by
list_midi_ports() - Refresh, Connect, Disconnect, Debug buttons
- Status line — reports connection state and any errors with enough detail to diagnose rebuild-required vs driver issues
- Load Map / Clear Map with file picker
- Active Mappings table with
_sectiondividers - Auto-reconnect on launch to last-used port name (stored in
localStorage) - Modal open/close wired before Tauri guard to ensure button always responds
- Port dropdown populated by
- MIDI engine (IIFE):
buildLookup(map)→ccLookup: Map<cc, [{paramId, type}]>applyCC(cc, val127, channel)— channel filter respectsmap.channel(-1 = any); dispatchesinput/change/clickas appropriateflashCtrl(paramId)— 120ms amber.midi-activeborder flash- Map persists to
localStoragekeyhuffMidiMap
MIDI map files (src/midi/)
FORMAT.md— schema reference, type behaviours, full parameter table, virtual port setup instructionsnanokontrol2.json— complete Korg nanoKONTROL2 Scene 1 factory map: 8 faders (CC 0–7), 8 knobs (CC 16–23), 8 solo buttons (CC 32–39, toggles), 8 mute buttons (CC 48–55), 8 rec buttons (CC 64–71), PLAY/STOP transport triggers (CC 41/42)nanokontrol1.json— Korg nanoKONTROL mk1 Scene 1 mapgeneric.json— 16-CC starter map (CCs 1–16)
Removed
src/midi-bridge.cjs— Node.js WebSocket sidecar (superseded by native Rust MIDI)easymidiandwsnpm dependencies (no longer needed)npm run midiscript
[1.0.2] — 2026-03-08
Added — Scanline parameters
Three new parameters in applyScanlines():
scanSpeed— global speed multiplier for drift and shift animation (range 0–5, default 1.0)scanGap— quantises band positions to a regular grid slot; 0 = continuous, higher values snap bands to intervals (range 0–200, default 0)scanSkew— position-based horizontal lean applied per band:shiftX += scanSkew × bandTop; creates a parallelogram lean across the scanline field (range −1–1, default 0)
Added — Cluster geometry parameters
Three new parameters in applyGlitch():
cluMinSpread— inner radius of the cluster placement annulus; tiles are forced outside this radius, creating a clear zone around each cluster centre (range 0–150, default 0)cluBias— fraction of tiles that must land inside clusters vs scattered randomly; 1.0 = all tiles cluster, 0.0 = fully random (range 0–1, default 0.85)cluDrift— noise-based drift applied to cluster centre positions each frame; creates slow organic movement without full physics (range 0–5, default 0)
Added — Cluster physics
Two new parameters replacing the previous static-only cluster placement:
cluSpeed— physics simulation velocity;0= legacy static mode (backwards-compatible), any positive value activates velocity integration (range 0–15, default 0)cluInertia— damping factor applied each frame:v = v × inertia + desired × (1 − inertia); higher values = heavier, slower response (range 0.01–0.99, default 0.92)
Implementation (effects.js):
- Module-level
let _cluPhysics = []andlet _cluPhysT = 0— physics state persists across frames getPhysicsCenters()— velocity integration with Perlin noise steering; wraps at canvas boundaries; used whencluSpeed > 0getStaticCenters()— original noise-offset behaviour; used whencluSpeed === 0(zero performance cost when disabled)- Centers returned as
{x, y}objects
Changed — UI layout
- PRESETS group moved above SOURCE — first visible group in the panel
- MOTION group merged into GLITCH — Speed, Fine, Mult, Glitch X/Y appended to bottom of the Glitch group; the separate Motion section was removed
- ↺ Reset Motion button added to the Feedback group — resets
fbX → 1,fbY → 1,fbZ → 1.000,fbTheta → 0.01
[1.0.1] — prior
Initial internal release. Core graphics pipeline, two-window Tauri architecture, WebSocket relay, preset system.
Dependency summary
| Package | Version | Layer | Purpose |
|---|---|---|---|
tauri |
1.x | Rust | Desktop shell, IPC, window management |
midir |
0.9 | Rust | MIDI input — CoreMIDI / ALSA / WinMM |
rosc |
0.10 | Rust | OSC packet decoding |
tokio |
1.x | Rust | Async runtime — WS relay + OSC UDP |
tokio-tungstenite |
0.21 | Rust | WebSocket server |
serde / serde_json |
1.x | Rust | JSON serialisation for Tauri events |
once_cell |
1.x | Rust | Static initialisation for shared state |
p5.js |
1.11.0 | JS | Canvas 2D rendering |
@tauri-apps/cli |
1.x | JS | Build tooling |
🚀 huff --- beta-v1.0.3 - MIDI
Changelog
Release date: 2026-03-09
Branch:beta
Files changed:src-tauri/src/main.rs·src-tauri/Cargo.toml·src-tauri/tauri.conf.json·src/index.html·src/midi/nanokontrol2.json·src/midi/nanokontrol1.json·src/midi/generic.json·src/midi/FORMAT.md
The graphics pipeline, WebSocket relay, preset system, and all effect parameters are unchanged.
Overview
This release adds a complete, production-ready MIDI system to huff. Any USB MIDI controller or software virtual port (IAC Bus, loopMIDI, Max/MSP, Pure Data) can now control every parameter in the app using a simple JSON map file. No MIDI learn, no hardcoded CC assignments — maps are plain text, portable, and shareable.
The implementation is entirely native to Tauri: MIDI runs in Rust via the midir crate (CoreMIDI on macOS, ALSA on Linux, WinMM on Windows), events are forwarded to the frontend via Tauri's own IPC, and nothing runs outside the app process. No sidecar, no Node addons, no second WebSocket.
Why Rust, Not the Web MIDI API
The Web MIDI API (navigator.requestMIDIAccess) does not function inside Tauri's WebView. WKWebView, WebView2, and WebKitGTK do not grant MIDI access because tauri:// is not treated as a secure context. The correct solution is to move MIDI entirely into Rust and forward events to JavaScript through Tauri's existing IPC mechanism — exactly what this release does.
Architecture
MIDI device (USB or virtual)
│
▼
midir (Rust, CoreMIDI / ALSA / WinMM)
│ parse_midi() → MidiEvent { kind, channel, data1, data2, value }
▼
window.emit("midi-event") ← Tauri IPC
│
▼
tauriEvent.listen("midi-event") ← index.html
│ ccLookup[cc] → { paramId, type }
▼
DOM element.value / .checked / .click()
│ dispatches input / change event
▼
canvas.js draw loop
New: Rust MIDI Commands
Four Tauri commands are exposed from main.rs. All are invoked from JavaScript via window.__TAURI__.invoke.
list_midi_ports() → string[]
Creates a fresh MidiInput instance and returns all port names visible to the OS at the moment of the call. Because a new instance is created each time, virtual ports created after the app launched (Max/MSP patches, Pure Data patches, IAC Bus routes) are always included in the list without requiring an app restart.
connect_midi_port_by_name({ portName }) → void
Connects to a port by name. Exact match is tried first; if no exact match is found, a case-insensitive substring match is used as fallback. This means passing "nanoKONTROL" will match "nanoKONTROL2 MIDI 1" without requiring the full system name. Once connected, every MIDI message from that port is forwarded to the frontend as a midi-event. Any previously open connection is closed before the new one opens.
disconnect_midi() → void
Closes the active connection and releases the port. Calling this is not required before connecting to a different port — connect_midi_port_by_name drops the previous connection automatically.
debug_midi_ports() → string
Prints all currently visible ports to stdout and returns the same text as a string. Useful when the Refresh list appears empty — the output confirms exactly what CoreMIDI / ALSA / WinMM can see and helps diagnose driver or permission issues.
midi-event payload
{
"kind": "cc",
"channel": 1,
"data1": 16,
"data2": 87,
"value": 0.685,
"raw": [176, 16, 87]
}kind is one of cc, note_on, note_off, pitch_bend, aftertouch, program_change, pressure, or unknown. value is always data2 / 127.0 — a normalised 0.0–1.0 ready for direct use as a uniform or blend factor.
New: MIDI Panel (index.html)
A MIDI button has been added to the topbar. Clicking it (or clicking the MIDI pill) opens the MIDI modal. The modal contains:
Device section — a port dropdown populated by list_midi_ports, a Refresh button, Connect, Disconnect, and a Debug button that writes all OS-visible ports to the status line and the browser console.
MIDI Map section — a file loader for JSON map files. The loaded map name is displayed. The active map persists in localStorage and is restored automatically on the next launch.
Active Mappings table — a live preview of every binding in the loaded map, organised by section headers, with CC number, parameter ID, type, and any author note.
The MIDI pill in the topbar shows the connected port name when active and flashes the CC number and value on every incoming message.
Auto-reconnect — on launch the engine silently attempts to reconnect to the last connected port name. If the port is available it connects without user interaction; if not, it reports the failure and waits for manual intervention.
Control flash — when a CC message moves a parameter, the corresponding .ctrl chip in the panel briefly highlights in amber so it is visually obvious which physical control maps to which on-screen parameter.
New: withGlobalTauri: true
tauri.conf.json now includes "withGlobalTauri": true in the build block. Without this flag, Tauri v1 does not inject window.__TAURI__ into the WebView, making all invoke and listen calls silently unavailable. This flag is required for any frontend code that uses the Tauri JavaScript API directly.
New: midir = "0.9" dependency
Added to src-tauri/Cargo.toml. midir is a cross-platform MIDI I/O library that wraps CoreMIDI (macOS), ALSA (Linux), and WinMM (Windows). It is the same library used by the reference template. All other existing Rust dependencies (tokio, tokio-tungstenite, serde, once_cell, etc.) are unchanged. The first build after this release will take longer than usual while Cargo fetches and compiles the new crate.
New: MIDI Map System
Maps are plain JSON files. They can live anywhere on disk and are loaded at runtime via the file picker — no installation, no copying to app directories.
Map format
{
"name": "My Controller",
"description": "Optional human note",
"version": 1,
"channel": -1,
"mappings": [
{ "param": "feedback", "cc": 0, "type": "range", "enabled": true },
{ "param": "corruptOn", "cc": 32, "type": "toggle", "enabled": true },
{ "param": "refreshBtn","cc": 41, "type": "trigger", "enabled": true }
]
}channel: -1 accepts messages from any MIDI channel. Set 0–15 to filter a specific channel.
Three type values are supported:
| Type | Behaviour |
|---|---|
range |
CC 0–127 scaled linearly to the parameter's min–max |
toggle |
CC > 63 → checked/ON · CC ≤ 63 → unchecked/OFF |
trigger |
Any CC value > 0 fires the target button's .click() |
Entries with "enabled": false are ignored by the engine but remain in the file for reference. _section keys are treated as visual comments in the mapping table and are never parsed as bindings.
Factory maps
Three maps ship in src/midi/:
nanokontrol2.json — full Scene 1 mapping for the Korg nanoKONTROL2. All 8 faders, 8 knobs, 8 solo buttons, 8 mute buttons, and 8 rec buttons are mapped. Transport PLAY and STOP are bound to refreshBtn and resetMotionBtn.
nanokontrol1.json — Scene 1 mapping for the original Korg nanoKONTROL (mk1).
generic.json — minimal 16-CC starter map suitable as a starting point for any controller. CCs 1–8 cover the primary mix parameters, CCs 9–12 cover texture, CCs 13–16 cover the main on/off toggles.
Full parameter reference (all 60+ mappable IDs with min/max ranges) is in src/midi/FORMAT.md.
Soft MIDI / Virtual Ports
Because list_midi_ports creates a fresh OS query on each call, virtual ports created after the app is running are immediately visible on the next Refresh.
macOS: Open Audio MIDI Setup → IAC Driver → check "Device is online". Any app that sends MIDI to the IAC Bus is received by huff.
Windows: Install loopMIDI (Tobias Erichsen). Create a virtual port. Max/MSP, Pure Data, Ableton, and any other MIDI-capable app can send to it.
Max/MSP / Pure Data: Route MIDI output to the virtual port and set huff to receive from the same port. CC messages from any patch will drive huff parameters in real time.
Validation Procedure
Two maps are provided for first-time validation:
nk2-validate.json — four controls only. Load it, touch each control, confirm the corresponding huff parameter responds:
| Physical control | CC | huff parameter | Expected result |
|---|---|---|---|
| Fader 1 | 0 | feedback |
Image smears up, clears down |
| Knob 1 | 16 | corrupt |
Glitch tiles appear / vanish |
| Solo 1 | 32 | corruptOn |
All glitch toggles on/off |
| Mute 1 | 48 | baseOn |
Source video toggles on/off |
If all four respond, load nanokontrol2-full.json for the complete surface.
Build Notes
A full Tauri rebuild is required to compile midir into the binary:
npm run dev
# or for a production build:
npm run buildThe first build after adding midir will take additional time. Subsequent builds are cached by Cargo and are no slower than before.
🚀 huff --- beta-v1.0.2
Changelog
Freeze date: 2026-03-08
Branch:beta
Files changed:src/index.html·src/canvas.js·src/effects.js
The graphics pipeline (draw loop, buffer management, compositing order) is unchanged.
Overview
This release is a pure UI and parameter-surface update. No draw-loop logic, buffer allocation, compositing order, or WebSocket relay code was modified. All changes are additive — existing presets load without breaking, and new parameters fall back gracefully to their defaults when missing from older preset JSON files.
The goals of this release were:
- Give clusters a full physics-driven motion system
- Expand scanline variability controls
- Surface more levers for cluster geometry
- Tidy the control panel layout
- Reduce friction for common reset actions
UI Layout Changes
PRESETS moved above SOURCE
The Presets group has been relocated from the bottom of the header to the top, directly beneath the transport bar. This means the first thing visible when the panel opens is preset management — load, save, and switch scenes before touching any individual parameter.
MOTION merged into GLITCH
The former standalone Motion section no longer exists as a separate collapsible group. Its five controls — Speed, Fine, Mult, Glitch X, and Glitch Y — now live at the bottom of the Glitch group. The underlying IDs and behaviour are identical; only the visual grouping changed. This reflects that these controls are part of the same tile-displacement system and belong together contextually.
New Control: Feedback — ↺ Reset Motion Button
A small ↺ Reset Motion button has been added inline inside the Feedback group. Clicking it resets the four motion sliders to their HTML defaults in a single action:
| Slider | Resets to |
|---|---|
| FB X | 1.000 |
| FB Y | 1.000 |
| FB Z | 1.000 |
| FB θ (Delta) | 0.01 |
Value labels update immediately on click. This is useful when feedback motion has drifted into an unwanted state during a live set and you need a quick, clean return to the neutral position without touching each slider individually.
Clusters — New Parameters
Three new geometry controls have been added to the Clusters group, alongside two new physics controls (see Physics section below).
MIN RAD
id: cluMinSpread · range: 0–150 · default: 0
Sets a minimum radius for tile placement around each cluster center. At 0, tiles can land anywhere from the center outward — producing tight blobs. Increasing this value pushes tiles away from dead-center, creating ring or donut-shaped cluster formations. Works in combination with Spread to define an inner and outer boundary: tiles are placed in the annular region between MIN RAD and SPREAD.
BIAS
id: cluBias · range: 0.0–1.0 · default: 0.85
Controls what fraction of the total tile count is forced into cluster zones. The remaining fraction is placed randomly across the grid.
- At
1.0— all tiles cluster tightly; no random scatter. - At
0.0— all tiles are placed at random; clustering is effectively disabled even when the Clusters ON toggle is active. - At
0.85(default) — the original behaviour is preserved with a small ambient scatter around the cluster formations.
This makes it possible to dial between a pure cluster mode and a hybrid scatter-with-bias mode without toggling the feature off.
DRIFT
id: cluDrift · range: 0–5 · default: 0
Applies a noise-driven positional offset to each cluster center that evolves over time. At 0 the centers are static (re-randomised each frame from the shared seed). As DRIFT increases the centers float slowly around the frame in organic, non-repeating paths driven by separate per-center noise offsets. When Speed (physics mode) is also active, DRIFT acts as a secondary perturbation added to the velocity vector each frame rather than a position offset.
Clusters — Physics System
SPEED
id: cluSpeed · range: 0–15 · default: 0
At 0, clusters behave exactly as in previous versions — centers are re-seeded each frame from the shared random seed and do not carry state between frames.
Above 0, the physics system activates. Each cluster center is given a persistent position and velocity vector that survive across frames. Every frame the velocity steers toward a direction sampled from a slowly-evolving noise field, then that velocity is integrated into the position. Centers wrap at screen edges rather than bouncing.
The result is cluster formations that drift and orbit continuously across the frame in smooth, organic trajectories — no teleporting, no repeating cycles.
Implementation note: Physics state is stored in a module-level
_cluPhysicsarray ineffects.js. The array is grown or shrunk to match the Centers count each frame, so adding or removing centers mid-session is safe. A module-level time accumulator_cluPhysTdrives the steering noise independently of p5'sframeCountandnPhaseX/Y, so physics motion is not coupled to or distorted by the glitch speed controls.
INERTIA
id: cluInertia · range: 0.01–0.99 · default: 0.92
Controls how much of the previous frame's velocity is retained before the new steering direction is blended in. Implemented as a simple exponential smoothing coefficient:
velocity = velocity × INERTIA + desiredVelocity × (1 − INERTIA)
- High values (→ 0.99): Centers carry heavy momentum and change direction slowly, producing long lazy arcs and slow orbital sweeps.
- Low values (→ 0.01): Centers snap almost instantly to the current noise direction — rapid, twitchy, nervous motion.
SPEED and INERTIA are complementary controls:
| Speed | Inertia | Character |
|---|---|---|
| Low | High | Slow heavy drift, wide arcs |
| Low | Low | Slow but jittery, short unpredictable hops |
| High | High | Fast smooth orbiting, sweeping trails |
| High | Low | Rapid chaotic scrambling across the frame |
Scanlines — New Parameters
Three new controls have been added to the Scanlines group, expanding the variability of the band displacement system.
SPEED
id: scanSpeed · range: 0–5 · default: 1.0
A global speed multiplier applied to both the vertical drift animation and the horizontal shift animation of every band. At 1.0 behaviour is identical to the previous version. Increasing this value makes bands move and shift more rapidly; reducing it toward 0 nearly freezes the bands in place while keeping all other parameters live.
GAP
id: scanGap · range: 0–200 · default: 0
At 0, band vertical positions are distributed continuously by noise (original behaviour). When GAP is set above 0, band positions are quantised to a regular slot grid of height band height + GAP. This enforces minimum vertical spacing between bands, producing a more structured, regularly-spaced striped appearance rather than the default organic clustering. Higher gap values create wider empty corridors between bands.
SKEW
id: scanSkew · range: −1.0–1.0 · default: 0
Adds a horizontal offset to each band that scales linearly with the band's vertical position on screen:
shiftX += SKEW × bandTopY
At 0 there is no skew (original behaviour). Positive values push bands at the bottom of the frame progressively further right compared to bands at the top, creating a diagonal lean to the displacement pattern. Negative values lean the opposite direction. Combined with Shift and Drift, this can produce a parallelogram-style shear or a V-shaped split across the frame.
canvas.js Changes
hookUI()element registry expanded to include all new IDs:cluMinSpread,cluMinSpreadVal,cluBias,cluBiasVal,cluDrift,cluDriftVal,cluSpeed,cluSpeedVal,cluInertia,cluInertiaVal,scanSpeed,scanSpeedVal,scanGap,scanGapVal,scanSkew,scanSkewVal.updateLabels()entries added for all new sliders.addEventListener('input', updateLabels)list updated to include all new range inputs.resetMotionBtnclick handler wired: resetsfbX,fbY,fbZ,fbThetato defaults and dispatchesinputevents to sync labels.PRESET_PARAM_IDSupdated to include all new parameter IDs so they are captured and restored by the preset system.
effects.js Changes
applyScanlines
- Reads three new parameters each frame:
scanSpeed,scanGap,scanSkew. - Drift and shift noise phase arguments scaled by
scanSpeedmultiplier. - Band vertical position optionally quantised by
scanGapslot grid. - Horizontal shift value augmented by
scanSkew × bTopbefore clamping. - Comment block updated to document new parameters.
applyGlitch — cluster geometry
- Reads three new parameters:
cluMinSpread,cluBias,cluDrift. - Tile placement now uses
cluBiasto split the tile count between cluster-forced and randomly-placed tiles. - Inner radius of cluster annulus is controlled by
cluMinSpread; outer radius remainscluSpread. - Reads two new physics parameters:
cluSpeed,cluInertia.
applyGlitch — cluster physics
- Module-level
_cluPhysicsarray and_cluPhysTaccumulator added at top of file. getPhysicsCenters()function: grows/shrinks_cluPhysicsto matchcluCenters, advances_cluPhysTbycluSpeed × 0.004per frame, steers each center via p5noise()field, applies INERTIA blending to velocity, integrates position, wraps at screen edges.cluDriftadds a secondary velocity perturbation when active.getStaticCenters()function: original noise-offset path, used whencluSpeed === 0.applyGlitchselects between the two paths:cluSpeed > 0 ? getPhysicsCenters() : getStaticCenters().- All returned center ob...
🚀 huff --- beta-v1.0.1
Changelog
First cross-platform release of huff-v2, a dual-window Tauri
application with a local WebSocket relay for real-time canvas streaming
between windows.
🧠 Overview
huff beta-v1.0.1 runs two coordinated windows:
- Main / Controls Window
- UI + p5 canvas
- Streams rendered frames to a local WebSocket relay
- Canvas Viewer Window
- Receives binary frame data
- Renders streamed output fullscreen
A lightweight Rust-based WebSocket relay runs inside the Tauri backend
and handles:
- Role-based client registration (
index/canvas) - Binary frame forwarding
- Automatic reconnect handling
All communication is local-only via:
ws://127.0.0.1:8787
No external networking required.
🔧 What's Included in v1.0.1
Core Backend (Rust)
- Integrated WebSocket relay (
tokio+tokio-tungstenite) - Binary frame streaming (JPEG/WebP)
- Role handshake system (
hellomessages) - IPv4 + IPv6 loopback binding
- Reconnect-safe architecture
tokio::sync::Mutexclient state management
Frontend
- Dual-window architecture
- Canvas mirror with fullscreen support
- Reconnect logic on WebSocket disconnect
- Status indicator for connection state
- Deterministic localhost WebSocket targeting
Cross-Platform
- Windows bundle (NSIS installer)
- macOS
.appbundle - macOS DMG packaging disabled for stability
- Unified Tauri v1 configuration across platforms
🛠 Build & Packaging Notes
- Windows: Generates NSIS installer
- macOS: Generates
.appbundle - WebSocket relay binds to
127.0.0.1:8787 - No admin privileges required
- Firewall permission may be required on first Windows launch
📦 Installation
Windows
Download the .exe installer and run normally.
macOS
Download the .app bundle and move to /Applications.
If macOS warns about unsigned developer: - Right-click → Open - Confirm
execution
🧩 Known Limitations
- DMG installer disabled (macOS builds
.apponly) - No code signing yet
- Localhost relay only (no remote streaming)
🔮 Roadmap
Planned improvements:
- Optional DMG packaging restoration
- Code signing
- Performance tuning for higher FPS streaming
- UI refinements
- Optional configurable WebSocket port
- Production packaging polish
🧪 Stability
This is a foundational cross-platform release.
Both Windows and macOS builds now: - Compile successfully - Bundle
correctly - Maintain WebSocket parity - Preserve identical runtime
behavior
🙏 Thanks
This release stabilizes cross-platform build behavior and real-time
canvas streaming architecture. Further iterations will focus on
refinement and production hardening.
beta-v1-macOS
beta-v1
Full Changelog: v1.4...beta-v1
v1.4 - MAC + CAMERA
- Successfully added camera source to application. Multiple sources tested, USB capture tested.
- Recording still disabled
- 2 screens fully integrated. Resizable, moveable
- Mac only, working on Windows version. Currently, the issue has to do with the tauri build process.
- Merged canvas and mirror files to reduce overhead.
v1.3 - MAC
- Second window implemented. Now it's canvas and control.
- WebSocket server implemented with Tauri for communication from controls to canvas
- Video playback works, but there is a loading issue. When you load a video, it will not load. If you load a second, and different video, it will load properly.
- Currently, no auto video playback when loaded.
- Needs more testing.
- Fullscreen rendering issues continue, so it only works within the window for now.
- Downloading may be disabled in this version