Skip to content

Releases: schwwaaa/huff

🚀 huff --- beta-v1.0.7

16 Mar 19:46

Choose a tag to compare

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 canvas role clients
  • Two-window architecture — controls in index.html, fullscreen output mirror in canvas.html
  • Syphon output (macOS) — Metal texture upload via SyphonMetalServer ObjC FFI; activated from the controls panel; visible as huff to any Syphon client (Resolume, VDMX, MadMapper, CoGe, etc.)
  • Spout output (Windows) — SpoutDX D3D11 pixel upload via spoutdx_send_image C-ABI bridge; activated from the controls panel; visible as huff to 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-event Tauri 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-message Tauri 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.md and src/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 in tauri.conf.json bundle.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.dll copied 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 installer

Full build instructions in README.md.

🚀 huff --- beta-v1.0.6

14 Mar 22:34

Choose a tag to compare

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.cmd

Binaries 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: ⏺ REC button 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: zip crate added for archive output
  • chore: Tauri dialog feature flag enabled for native Save / Pick-folder dialogs
  • chore: scripts/download-ffmpeg.sh + scripts/download-ffmpeg.cmd for 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

Built with Tauri v1 · p5.js · Syphon

🚀 huff --- beta-v1.0.5

09 Mar 08:18

Choose a tag to compare

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() — loads Syphon.framework at runtime via NSBundle. 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 a MTLDevice (system default GPU), MTLCommandQueue, and SyphonMetalServer named "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-byte HUFFSYPH header, creates a MTLTexture with shared storage mode (zero-copy on Apple Silicon), uploads RGBA pixels via replaceRegion:mipmapLevel:withBytes:bytesPerRow:, then publishes via publishFrameTexture:onCommandBuffer:imageRegion:flipped:YES. flipped: YES corrects 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 via status().
  • 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 because msg_send! expands internally to sel! calls that must be in crate scope; a use import in the child module is insufficient due to how macro_rules! resolution works.
  • Binary WS handler — added HUFFSYPH intercept: frames with the magic prefix are routed to syphon::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_id moved 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 to tauri.macOS bundle block. Path is relative to src-tauri/. Tauri copies the framework to Contents/Frameworks/ at build time.

src-tauri/frameworks/Syphon.framework — bundled

  • Universal binary (x86_64 + arm64), full Versions/A structure with _CodeSignature.
  • Includes default.metallib (Metal shader library required by SyphonServerRendererMetal).

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 before getImageData(), so the Syphon output is always exactly the specified dimensions regardless of canvas window size.
    • rAF loop respects FPS cap via timestamp delta.

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 — compiles aarch64-apple-darwin, auto-installs Rust target if missing.
  • build_mac_x86 — compiles x86_64-apple-darwin.
  • build_mac_universal — runs both arch builds, then assembles a universal .app and .dmg manually:
    • thin_bundle_frameworks(app, arch) — strips every fat Mach-O in Contents/Frameworks/ to a single arch using lipo -thin before the combine step. This solves the Tauri universal bundler limitation: Tauri's lipo pass cannot merge two fat binaries with overlapping architectures (the Syphon.framework binary is already universal). By thinning each arch bundle first, the subsequent lipo -create succeeds cleanly.
    • lipo -create pass — walks all Mach-O files in the ARM bundle and merges with x86_64 counterparts.
    • Optional create-dmg step — produces a distributable .dmg if create-dmg is installed (brew install create-dmg).
  • build_windows — Windows x86_64 via native MSVC or cargo-xwin / cross for cross-compilation from macOS.
  • Written for bash 3.2 (macOS system shell) — no declare -A or 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 via app.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(), localStorage map 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 sidecar
  • MidiEvent { 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": true added to tauri.conf.json — required for window.__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(), localStorage map 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, ws npm dependencies
  • npm run midi script

[1.0.2] — 2026-03-08 — Scanlines, Cluster Physics, UI

Added

  • scanSpeed, scanGap, scanSkew — scanline animation parameters
  • cluMinSpread, cluBias, cluDrift — cluster geometry parameters
  • cluSpeed, cluInertia — cluster physics simulation (velocity integration with Perlin steering; cluSpeed === 0 preserves 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

09 Mar 05:05

Choose a tag to compare

Changelog

Added — OSC (Open Sound Control)

Rust (src-tauri/src/main.rs)

  • Added rosc = "0.10" to Cargo.toml — pure-Rust OSC decoder, no system dependencies
  • run_osc_listener() — async Tokio task that binds 0.0.0.0:9000 at app startup, receives UDP packets, and decodes them via rosc::decoder::decode_udp()
  • dispatch_osc() — recursively handles both bare OscPacket::Message and OscPacket::Bundle (bundles containing multiple messages are fully unwrapped)
  • OscEvent struct — 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 — returns 9000, lets the frontend display the active port without hardcoding it in JS
  • OSC_SHUTDOWN static OnceCell — holds a oneshot::Sender to cleanly stop the listener if needed
  • Listener is spawned inside .setup() alongside the existing WebSocket relay; the AppHandle is passed in so emit_all() reaches all windows

Frontend (src/index.html)

  • OSC button added to topbar — opens the OSC modal
  • OSC pill added to topbar — shows :9000 when 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; _section keys 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) — builds addrLookup: Map<addr, [{paramId, type, inputMin, inputMax}]>
    • applyOsc(addr, rawValue) — normalises inputMin–inputMax to 0–1, then scales to element min–max; dispatches input event for ranges, change for toggles, click for triggers
    • flashCtrl(paramId) — 120ms green .osc-active border flash on the target .ctrl chip
    • Map persists to localStorage key huffOscMap and 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, Python
  • touchosc-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 dependencies
  • osc-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" to Cargo.toml — cross-platform MIDI I/O (CoreMIDI / ALSA / WinMM)
  • MidiEvent struct — serialised and emitted as "midi-event": { kind, channel, data1, data2, value, raw }
    • kindcc, note_on, note_off, pitch_bend, aftertouch, program_change, pressure, unknown
    • valuedata2 / 127.0, normalised 0.0–1.0
  • parse_midi() — decodes raw MIDI bytes into MidiEvent; handles 0x90 velocity-0 as note-off; normalises 14-bit pitch bend to 0–127
  • MIDI_CONNLazy<Mutex<Option<MidiInputConnection>>> global that keeps the connection alive between Tauri commands
  • list_midi_ports() — creates a fresh MidiInput on each call so virtual ports (IAC Bus, loopMIDI, Max/MSP, Pure Data) created after launch appear in the list without restart
  • connect_midi_port_by_name({ portName }) — exact match first, then case-insensitive substring fallback; drops any existing connection before opening the new one
  • connect_midi_port({ portIndex }) — legacy index-based connect retained for compatibility
  • disconnect_midi() — drops the MidiInputConnection, releasing the port back to the OS
  • debug_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": true to the build block — required for window.__TAURI__ to be injected into the WebView in Tauri v1. Without this flag all invoke() and listen() calls silently fail.

Frontend (src/index.html)

  • MIDI button added to topbar
  • MIDI pill added to topbar — shows connected port name; flashes CC N=V on 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 _section dividers
    • Auto-reconnect on launch to last-used port name (stored in localStorage)
    • Modal open/close wired before Tauri guard to ensure button always responds
  • MIDI engine (IIFE):
    • buildLookup(map)ccLookup: Map<cc, [{paramId, type}]>
    • applyCC(cc, val127, channel) — channel filter respects map.channel (-1 = any); dispatches input/change/click as appropriate
    • flashCtrl(paramId) — 120ms amber .midi-active border flash
    • Map persists to localStorage key huffMidiMap

MIDI map files (src/midi/)

  • FORMAT.md — schema reference, type behaviours, full parameter table, virtual port setup instructions
  • nanokontrol2.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 map
  • generic.json — 16-CC starter map (CCs 1–16)

Removed

  • src/midi-bridge.cjs — Node.js WebSocket sidecar (superseded by native Rust MIDI)
  • easymidi and ws npm dependencies (no longer needed)
  • npm run midi script

[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 = [] and let _cluPhysT = 0 — physics state persists across frames
  • getPhysicsCenters() — velocity integration with Perlin noise steering; wraps at canvas boundaries; used when cluSpeed > 0
  • getStaticCenters() — original noise-offset behaviour; used when cluSpeed === 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

09 Mar 04:27

Choose a tag to compare

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 015 to filter a specific channel.

Three type values are supported:

Type Behaviour
range CC 0–127 scaled linearly to the parameter's minmax
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 build

The 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

09 Mar 02:24

Choose a tag to compare

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 _cluPhysics array in effects.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 _cluPhysT drives the steering noise independently of p5's frameCount and nPhaseX/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.
  • resetMotionBtn click handler wired: resets fbX, fbY, fbZ, fbTheta to defaults and dispatches input events to sync labels.
  • PRESET_PARAM_IDS updated 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 scanSpeed multiplier.
  • Band vertical position optionally quantised by scanGap slot grid.
  • Horizontal shift value augmented by scanSkew × bTop before clamping.
  • Comment block updated to document new parameters.

applyGlitch — cluster geometry

  • Reads three new parameters: cluMinSpread, cluBias, cluDrift.
  • Tile placement now uses cluBias to split the tile count between cluster-forced and randomly-placed tiles.
  • Inner radius of cluster annulus is controlled by cluMinSpread; outer radius remains cluSpread.
  • Reads two new physics parameters: cluSpeed, cluInertia.

applyGlitch — cluster physics

  • Module-level _cluPhysics array and _cluPhysT accumulator added at top of file.
  • getPhysicsCenters() function: grows/shrinks _cluPhysics to match cluCenters, advances _cluPhysT by cluSpeed × 0.004 per frame, steers each center via p5 noise() field, applies INERTIA blending to velocity, integrates position, wraps at screen edges. cluDrift adds a secondary velocity perturbation when active.
  • getStaticCenters() function: original noise-offset path, used when cluSpeed === 0.
  • applyGlitch selects between the two paths: cluSpeed > 0 ? getPhysicsCenters() : getStaticCenters().
  • All returned center ob...
Read more

🚀 huff --- beta-v1.0.1

03 Mar 08:01

Choose a tag to compare

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 (hello messages)
  • IPv4 + IPv6 loopback binding
  • Reconnect-safe architecture
  • tokio::sync::Mutex client 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 .app bundle
  • macOS DMG packaging disabled for stability
  • Unified Tauri v1 configuration across platforms

🛠 Build & Packaging Notes

  • Windows: Generates NSIS installer
  • macOS: Generates .app bundle
  • 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 .app only)
  • 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

03 Mar 05:14
9bf045f

Choose a tag to compare

beta-v1

Full Changelog: v1.4...beta-v1

v1.4 - MAC + CAMERA

17 Oct 01:51

Choose a tag to compare

  • 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

28 Sep 01:03

Choose a tag to compare

  • 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