Desktop music visualizer — WebGPU rendering, deterministic exports, local-first.
Download · Docs · Add a visual mode · Themes
Tauri 2 + React + TypeScript, WebGPU rendering (Canvas2D fallback), Rust core.
Free and open source. Built to become a professional-grade tool for producers and artists — local-first, no cloud rendering, no watermarks, no subscriptions.
- Local file playback (mp3/flac/wav/ogg/m4a) via Web Audio — drag & drop or file picker; whole-track or draggable A-B region looping with sample-accurate Web Audio endpoints
- Configurable log/linear spectrum analysis with 85/171/341 ms-class drawn-spectrum resolution, optional measured FFT bins with no interpolation, asymmetric smoothing, peak hold, band energies, spectral-flux beat detection, slow energy envelope, phase-locked waveform. Longer display FFTs never retune onset detectors
- 16 visual modes (WebGPU shader presets): Spectrum Bars, Radial Burst,
Oscilloscope, Particles, Tunnel, Kaleido Nebula, Metaballs, LED Matrix,
Voice Orb (narration mode), Echo Trails (feedback), Particle Flow (120k GPU
compute particles), Spectrum Scape (3D), Aurora, Synthwave,
Bass Circle (circular bass visualizer with album art), and Builder — a real
layer compositor: stack up to twelve layers, each with its own blend mode,
color and parameters; share stacks as
.bfbuilderfiles - Every mode but Builder: 6-14 curated factory styles, plus its controls sorted into groups (~430 app-wide). Each group leads with the controls that change the look and folds its internal constants behind its own expert line, closed by default and counting what you have changed; Show every control opens them all at once. Plain-language hint for every control (tooltip + live hint bar), all persisted per mode. Builder carries six whole-stack starting points instead of style chips
- Visuals dock (press G): a persistent, keyboard-resizable right-hand column the visual runs full width behind — tune while you watch. One vertical section rail of eight destinations (Mode, Global motion, Looks & themes, Sync, Modulation, Scene, Text, Live), each a page; a header naming the current mode and style; a search box that finds any control by name across every page, expert controls included. Stage mode (S) hides it entirely
- User looks: save your own named looks per visual mode on Looks &
themes, share them as
.bfpresetfiles (import/export) - Gallery: browse a public, curated collection of community looks and themes right in the app (top-bar button) — every entry pinned to an immutable version and checksum-verified before it is ever parsed; add a look or apply a theme in one click. Submissions are reviewed on GitHub (beatform-app/gallery)
- Shadertoy import: paste a single-pass Shadertoy shader (the Image tab)
and it becomes a Beatform visual — translated to WGSL locally, audio on
iChannel0in Shadertoy's own music-texture layout, track-clock time so exports match previews exactly, author/license attribution kept with the visual and its.bfshaderexports - Auto-updates: the app checks GitHub Releases and installs new versions in one click (signature-verified; no telemetry — the check is a plain fetch of a static file). Preferences (Ctrl+,), four tabs: General (autosave delay, remembered save-dialog folder), Modes (drag the mode strip into your own order), Performance (live frame cap, preview resolution, GPU preference, and a Performance display FPS/CPU/memory overlay drawn over the preview and never into it), Updates
- Project files: save/open the whole setup (preset, params, sync,
background, layers) as versioned
.bfprojfiles — Ctrl+S / Ctrl+O, native dialogs - Overlay layers: text with
{title}/{artist}auto-fill from tags, logos/images, one-click album art — anchored, resolution-independent, resolved from the same project document in exports - Frame aspects (Fill / 16:9 / 9:16 / 1:1) with letterboxed preview and aspect-matched export resolutions up to vertical 4K
- Spotify Canvas mode: pick any 3-8 s segment, export a 1080×1920 seamless loop (tail crossfades into the head — invisible loop point)
- Loudness meter: momentary LUFS (ITU-R BS.1770) live readout; stereo width feature for presets
- Musical analysis on every track (background worker): BPM + beat grid (visuals get beat/bar phase), kick/snare/hat onset classes as sync sources, key detection (Krumhansl), section boundaries as seek-bar ticks
- Modulation matrix: target-first cards routing any audio feature (drums, bands, width, beat/bar phase, section change, the lyric line, an imported stem's bands) or one of eighteen beat-synced LFOs onto any knob of the active visual and the whole post chain — with depth, response curve, rise/fall, six one-click recipes, live per-source meters, and a driven mark on the sliders themselves. Resolved by the same track-time functions in exports
- Smooth curve toggle: spline-connected spectrum (Catmull-Rom through the bins) instead of hard-edged bars, across all visuals
- Timeline workstation (press T): arrange scenes (any visual per song part) with beat-snapped drag, seven scene transitions (crossfade, two wipes, iris, zoom, glitch, hard cut), and keyframe automation lanes for any parameter — against a waveform overview with a beat/bar ruler and section markers. Exports resolve the arrangement from deterministic track-time timestamps.
- Undo/redo (Ctrl+Z / Ctrl+Y) across every edit, with gesture grouping (a slider drag is one step); crash-safe autosave on desktop, offered back on the next launch if the app died with unsaved work
- Sync-source system: choose what visuals react to (kicks, energy, bass, melody, voice, treble, snare, hats) + smoothing/attack/release, saved per mode
- Background system on every preset: preset-animated, any solid color (incl. chroma green/magenta swatches), transparent (luma alpha, checkerboard preview), an image (or album art), or a looped video (desktop; deterministic frame-by-track-time so exports match). Image and video share a Fill/Fit/Stretch framing row with zoom and pan, and a scope switch gives any single mode its own background
- MP4 export: offline-rendered WebCodecs pipeline (H.264 + AAC, hardware encode, faster than realtime) running in a worker — the UI never freezes. On desktop, exports stream straight to disk (fragmented MP4, flat memory — hour-long renders are fine). Export A/V timestamps share one decoded-audio clock, so drift cannot accumulate. Preview and export share DSP/render code but live device timing is not numerically identical; see the preview/export truth contract. 720p→4K / 30/60 fps / auto or manual bitrate. HEVC and AV1 where the hardware supports them (probed; codec choice does not change the raw render). Design: docs/EXPORT-DESIGN.md
- Transparent WebM export — VP9 with a real alpha channel (color + alpha planes muxed via BlockAdditions) + Opus audio, for OBS overlays and web embeds. Pick the VP9 + alpha codec, set Background to Transparent
- PNG sequence export with alpha — numbered frames into a folder, keeping transparency for compositing in Premiere/Resolve/After Effects
- ProRes 4444 export (desktop): one .mov with alpha + untouched PCM audio — the editorial mezzanine that drops straight into an NLE. Encoded by a bundled LGPL ffmpeg sidecar (separate binary; see THIRD_PARTY_LICENSES.md); frames stream from the renderer into ffmpeg, so memory stays flat
- AV1 10-bit export (desktop): genuine 10-bit MP4 (yuv420p10le, BT.709) — the render is tapped at 16-bit float before the 8-bit swapchain, so smooth gradients keep all their levels instead of banding. Raw 64-bit frames stream into the bundled ffmpeg's SVT-AV1 encoder; memory stays flat
- GIF / animated WebP export (desktop): seamless loop files via the bundled ffmpeg — GIF for anywhere, WebP for small files with alpha. Pairs with Canvas loop mode
- Batch render: drop in 20 tracks, get 20 titled videos — one per track, unattended. Each title comes from that file's own ID3 tags, so there is no spreadsheet and no retyping; anything untagged falls back to the filename and is flagged so you can fix it in place. Everything else (preset, layers, timeline, post, loudness) is whatever you have set up. One job at a time, and a file that fails costs that one video rather than the night
- Loudness normalization on export: match the audio to −14 LUFS (streaming), −16 (podcast) or −23 (EBU R128), measured per ITU-R BS.1770-4 and held under a −1 dBTP ceiling by a look-ahead true-peak limiter, so nothing clips when a streaming service re-encodes it. Audio-only — toggling normalization does not change that export's visual analysis or raw frames. Off by default
- Timed lyrics (.lrc / .srt): drop a lyrics file on the window and the current line renders karaoke-style over the visual — position, size, color and fades configurable from the same timed definition in every export
- Music library (desktop): pick your music folder once — every track listed with its real tags (title/artist/duration via lofty), one click to play, and finished tracks flow into the next near-gaplessly (the next file is read and decoded while the current one plays)
- Listen to the system (desktop): WASAPI loopback visualizes whatever the PC is playing — Spotify, a browser, a DAW — live, without touching a file. Analysis-only tap; nothing is re-emitted to the speakers
- Live performance / VJ: jump between modes with number keys 1–9 or a MIDI controller, with optional beat-quantized takeover (the switch lands on the next beat/bar, Ableton-style); Stage mode (S) gives a clean chrome-free full-bleed output with blackout and a mode-name HUD; Web MIDI maps knobs to any parameter and notes to modes (local, no drivers). Preview-only — never affects exports
- Product chrome: auto-hides while playing (the Visuals dock stays lit), keyboard shortcuts (press H — every performance shortcut has a letter or digit as its primary binding, so it works on every keyboard layout; the punctuation aliases are bound by physical key position), hover-scrub seek bar, onboarding empty state, GPU-loss auto-recovery
- Three synthesized demo tracks (120 BPM house / 174 BPM DnB / 70 BPM ambient) for instant cross-style testing without files
src/
audio/
engine.ts AudioContext graph, decoded-buffer playback, seek/volume
featurePipeline.ts source-agnostic spectrum->AudioFeatures (deterministic)
realtimeSource.ts live analysis — shared DSP, device-timed input
offlineSource.ts PcmData driver at fixed fps (export path)
dsp/fft.ts own real FFT (Hann), shared by live + offline paths
types.ts AudioFeatures — the audio->render contract
demoTrack.ts OfflineAudioContext demo synth
render/
types.ts Renderer + Preset interfaces, param schemas (serializable)
webgpuRenderer.ts fullscreen-triangle pass, shared WGSL header/ABI
canvas2dRenderer.ts fallback renderer, same interface
presets/ one file per visual; index.ts is the registry
export/
exportCore.ts env-agnostic render+encode+mux pipeline
exportWorker.ts module worker running the core off the main thread
videoExporter.ts orchestration: worker/inline, blob or stream-to-disk
state/
store.ts zustand store: document slice (project payload) + session
services.ts engine/analyzer/renderer singletons + frame loop
project.ts .bfproj schema, validation, migration point
userPresets.ts .bfpreset user looks
platform.ts Tauri/browser file dialogs + IO
persistence.ts localStorage cache (last session)
App.tsx view layer over the store
docs/EXPORT-DESIGN.md offline-rendered, frame-perfect MP4 export design
src-tauri/ Rust shell — dialog/fs plugins, library scan
(walkdir + lofty), WASAPI loopback capture (cpal)
Design rules: renderers consume only AudioFeatures; presets declare params
as schema and stay pure functions of (features, time, params) — purity is
what makes the indexed offline frame walk repeatable. Live device sampling and
cross-hardware pixels remain measured parity, not identity. New visual = one
preset file + registry entry. Document state lives in the store's document
slice and is what .bfproj serializes.
npm install
# one-time, none of these are in git:
node scripts/fetch-ffmpeg.mjs # ffmpeg sidecar (~110 MB) — ProRes/AV1/GIF/WebP
node scripts/fetch-whisper.mjs # whisper.cpp runtime (lyrics)
node scripts/fetch-onnxruntime.mjs # onnxruntime + DirectML (lyrics)
node scripts/build-lyrics-sidecar.mjs # lyrics sidecar exe — required before any cargo
# command works (the bundle resource must exist)
npm run dev # browser dev at localhost:1420 (fastest iteration)
npm run tauri dev # full desktop shell
npm run tauri build # installer (needs every sidecar above)
npm test # vitest (DSP, schemas, golden traces)
npm run lint # eslint
npm run typecheck # tsc --noEmit
The full gate list — web, Rust, and the device/E2E suites with the rules for
when each is mandatory — is GATES.md. CI runs the web gates
(typecheck, lint, format check, tests, build) on every push/PR, plus a desktop
job (cargo fmt --all, cargo clippy --workspace, cargo test --workspace)
and a dependency audit. Always pass --workspace/--all to cargo: bare cargo
silently skips the lyrics-sidecar member.
The foundations → workstation → visual-ceiling → pro-delivery → ecosystem → storytelling → motion arc is complete and shipped; live-performance ("Stage") is the current frontier — beat-quantized switching, Web MIDI and Stage mode have landed, with a true second-display output window still to come. Everything stays free and open source, GitHub-only, with no monetization.
Current work, evidence gaps, feature candidates and explicit non-goals are kept
in BACKLOG.md. That ledger is the canonical detailed queue;
older local roadmap and plan files are historical records.
The full on-hardware acceptance pass is complete — every item in
TESTING.md is green, including real-hardware drag-and-drop, a physical
non-US keyboard, a two-hour export soak and ProRes 4444 alpha round-trip.
v3.0.0 is not a version bump waiting on a checklist. It is the point where
this is exactly the app it should be — every feature something to stand behind,
not merely something that works. Passing the acceptance pass is evidence toward
that, not a trigger for it. Development continues on the 2.x line.