# Introduction **FFT Visualizer** is a high-performance, WebGL-based real-time audio **spectrum analyzer** and **FFT visualizer** for the web. The entire visual — bars, LED segments, radial layout, gradient, glow, reflection — is drawn by a **single fragment shader in one GPU draw call**, so it stays smooth even at 120 fps with 80 bands in stereo. ::note It ships as three packages from [one monorepo](https://github.com/harmonics-audio/fft-visualizer){rel=""nofollow""} : [`@fft-visualizer/core`](https://www.fftvisualizer.com/core/installation) — a framework-agnostic vanilla-TypeScript class you drop onto a `` in any framework, or none — plus two thin wrappers around it, [`@fft-visualizer/vue`](https://www.fftvisualizer.com/vue/installation) and [`@fft-visualizer/react`](https://www.fftvisualizer.com/react/installation) . **This Guide covers the shared concepts;** the Core, Vue and React tabs cover each package's own API. :: ## Highlights - **WebGL fragment-shader rendering** — the whole spectrum is one draw call; no per-bar canvas ops. - **Three data sources** — capture audio locally (mic or tab/system), stream pre-computed FFT over WebSocket, or feed your own data. - **In-browser FFT** — optional Rust/WASM FFT processor, lazy-loaded only when you capture audio locally. - **Rich visual modes** — LED segments (two styles), radial, stereo, mirrored reflection, glow, rotation, per-level or per-axis coloring. - **Flexible gradients** — 10 built-in presets or custom stops in any CSS color format. - **Zero rendering dependencies** — the whole visual is native WebGL; no charting library, no canvas 2D. - **Framework-agnostic** — a vanilla-TS core with thin per-framework wrappers for Vue and React. ## Why this instead of a canvas visualizer? Most spectrum visualizers (e.g. audioMotion-analyzer) draw to a 2D canvas. FFT Visualizer renders the entire frame — every bar, LED gap, gradient, glow and reflection — in a **single WebGL fragment shader / draw call**, which keeps it cheap at high band counts and frame rates. It also ships a **WebSocket remote-FFT protocol** with reference servers (Python, Node.js, Rust), so the FFT can run on a separate device — a Raspberry Pi, a media server — and the browser only draws. That's a case canvas visualizers don't cover. ## Browser support Requires WebGL (all modern browsers): Chrome 56+, Firefox 51+, Safari 15+, Edge 79+. Local capture additionally needs `getUserMedia` / `getDisplayMedia`. # How it works FFT Visualizer is, at its core, a **renderer**: give it an array of magnitudes and it draws a spectrum on the GPU. Everything before that — capturing audio and turning it into frequency data — is *pluggable*, and **where the FFT runs depends on the `mode`** you pick. ## The pipeline ```text mode="local" mic / tab audio │ getUserMedia() / getDisplayMedia() ▼ Web Audio: AudioContext ▸ MediaStreamSource ▸ AnalyserNode │ getFloatTimeDomainData() ← raw waveform (e.g. 2048 samples) ▼ Rust / WASM FftProcessor.process() Hann window ▸ real FFT ▸ magnitude ▸ log-spaced bands ▸ A-weighting │ ▼ ┌───────────────────────────────┐ mode="websocket" │ Uint8Array — 0–255, │ ◀── a backend server streams this mode="external" │ one value per frequency band │ ◀── you push this via props └───────────────────────────────┘ │ ▼ noise floor ▸ smoothing ▸ peak-hold ▸ aggregate to `bands` ▼ WebGL fragment shader — one draw call │ ▼ bars on screen ``` The `Uint8Array` of 0–255 magnitudes is the **hand-off point**. In `local` mode the visualizer produces it; in `websocket` and `external` modes it arrives ready-made and the FFT is **not** part of the visualizer at all. ## Where the FFT runs | Mode | Who computes the FFT | Runs where | | ----------- | -------------------------------------------- | --------------------- | | `local` | The bundled Rust/WASM processor | In the browser | | `websocket` | Your backend (Python / Node / Rust examples) | On a server or device | | `external` | Your own code (Web Audio, another analyser…) | Wherever you like | So if you're wondering "is the audio→FFT conversion part of FFTVisualizer?" — only in `local` mode. See [Data modes](https://www.fftvisualizer.com/guide/data-modes) for the API of each. ## Inside `local` mode When you set `mode` to `local`, the visualizer runs the whole chain client-side: 1. **Capture** — `getUserMedia()` (microphone) or `getDisplayMedia()` (tab/system audio) returns a `MediaStream`. 2. **Web Audio graph** — `AudioContext → MediaStreamAudioSourceNode → AnalyserNode`. 3. **Grab raw samples** — each animation frame it reads `analyser.getFloatTimeDomainData()`, which returns the **raw waveform** (the audio samples themselves), *not* frequency data. 4. **FFT in Rust/WASM** — those samples go to `FftProcessor.process()`, which: - applies a **Hann window** (reduces spectral leakage), - runs a **real FFT**, - takes the **magnitude** of each complex bin (normalised by FFT size), - groups bins into **exponentially (log-)spaced frequency bands**, so bass and treble get perceptually fair spacing instead of linear, - applies **A-weighting** (the loudness curve that matches how the ear weights frequencies). 5. The result is a `Uint8Array` of 0–255 values — one per band — ready for the shader. The WASM module is **lazy-loaded**: it's only fetched the first time you actually capture audio locally, so `websocket`/`external` users never download it. ::note{title="Why not use the browser's built-in FFT?"} `AnalyserNode` can compute the FFT for you ( `getByteFrequencyData` ). FFT Visualizer deliberately grabs raw **time-domain** samples and runs its **own** FFT instead, for two reasons: **identical output everywhere** — the exact same Rust code runs in the browser (WASM) *and* in the [backend reference servers](https://www.fftvisualizer.com/guide/websocket-protocol#reference-servers) , so a local mic demo and a remote stream look the same — and **control** the browser doesn't expose: custom windowing, log-spaced bands, A-weighting and a fixed frequency range (100 Hz–18 kHz). The `AnalyserNode` here is used purely as a convenient way to read a buffer of samples; its own FFT is ignored. :: ## Two resolutions: `bins` vs `bands` There are two spectrum sizes in the chain, and it's worth keeping them straight: - **`bins`** — the number of frequency bands the FFT processor (or your server) emits. - **`bands`** — how many bars the visualizer actually *displays*. It aggregates the incoming `bins` down to `bands` before rendering. They're often the same (e.g. 80 → 80), but you can render fewer bars than you receive — for example feed 80-bin data but display `bands: 28`. ::tip Every mode converges on the **same** 0–255 array before rendering — so the look you configure is driven by identical data whether it came from a local mic, a remote server, or your own pipeline. Want that array back out — to drive LEDs, a flip-dot display, or any external hardware? The `frame` event ( [Core](https://www.fftvisualizer.com/core/api#events) , [Vue](https://www.fftvisualizer.com/vue/props#the-frame-event) , [React](https://www.fftvisualizer.com/react/props#onframe) ) hands you exactly what the shader draws, every frame. :: # Data modes The visualizer has one job — render a spectrum — and three ways to get the data, selected with the `mode` option (a prop in Vue and React, a constructor option in Core). | Mode | How data arrives | Use when | | --------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `local` | Captures mic or display audio and computes the FFT in-browser (Rust/WASM) | You want a zero-backend, client-side visualizer | | `websocket` *(default)* | Connects to `websocketUrl` and reads **pre-computed** FFT frames | A server/device already produces FFT data (e.g. a Raspberry Pi) | | `external` | You pass FFT magnitudes as `data` / `dataLeft` / `dataRight` | You have your own audio pipeline (Web Audio, another analyser, etc.) | ## Local mode Set `audioSource` to `display` to capture tab/system audio via screen sharing instead of the microphone. ::code-group ```ts [Core] new FFTVisualizer(canvas, { mode: 'local', audioSource: 'mic', bands: 80 }) ``` ```vue [Vue] ``` ```tsx [React] ``` :: ## WebSocket mode The server streams a small config message then binary FFT frames — see the [WebSocket protocol](https://www.fftvisualizer.com/guide/websocket-protocol). Reference servers for Python, Node.js and Rust are in the repository's `backend-examples/`. ::code-group ```ts [Core] new FFTVisualizer(canvas, { mode: 'websocket', websocketUrl: 'wss://your-server/fft', bands: 40, autoReconnect: true }) ``` ```vue [Vue] ``` ```tsx [React] ``` :: ::warning On an HTTPS page you must use a **`wss://`** URL — browsers block insecure `ws://` from secure pages as mixed content. :: ## External mode Pass a `Uint8Array` of magnitudes (0–255); update it and the visual follows. For stereo, pass `dataLeft` and `dataRight` instead of `data`. ::code-group ```ts [Core] const viz = new FFTVisualizer(canvas, { mode: 'external', bands: 40 }) // ...each frame, feed fresh magnitudes (the data is copied): viz.feedData(myUint8Array) ``` ```vue [Vue] ``` ```tsx [React] import { useState } from 'react' import { FFTVisualizer } from '@fft-visualizer/react' const [data, setData] = useState(new Uint8Array(80)) // ...set a fresh array from your own analyser each frame ``` :: ::tip **Feed data by copy for in-place buffers.** Core's `feedData()` copies the data, so reusing one `Uint8Array` each frame is fine. In both wrappers the `data` prop is compared **by reference** — if you mutate the same array in place, the change goes unnoticed. Either pass a fresh array, or call `feedData()` on the component ( [Vue](https://www.fftvisualizer.com/vue/props#exposed-methods) , [React](https://www.fftvisualizer.com/react/props#imperative-handle) ), which copies — and in React also skips a render per frame. :: # Options Every package configures the **same** set of options — the framework-agnostic `FFTVisualizerOptions`. This page is the single source of truth for what each one **means**; the [Core API](https://www.fftvisualizer.com/core/api), [Vue props](https://www.fftvisualizer.com/vue/props) and [React props](https://www.fftvisualizer.com/react/props) pages link back here for descriptions. ::note How you pass them differs by package: **Core** takes an options object in the constructor and `setOptions()` ( `{ ledBars: true }` ); **Vue** takes them as props in `camelCase` or `kebab-case` ( `:led-bars="true"` ); **React** takes them as `camelCase` props ( `ledBars` ). The names and meanings below are identical either way. :: ## Data source | Option | Type | Default | Meaning | | ------------------------ | ------------------------------------ | ------------- | -------------------------------------------------------------------------------------------- | | `mode` | `'websocket' | 'local' | 'external'` | `'websocket'` | Where FFT data comes from — see [Data modes](https://www.fftvisualizer.com/guide/data-modes) | | `websocketUrl` | `string` | — | WebSocket URL (used when `mode: 'websocket'`) | | `data` | `Uint8Array` | — | External FFT magnitudes, mono (`mode: 'external'`) | | `dataLeft` / `dataRight` | `Uint8Array` | — | External FFT magnitudes per channel (stereo external mode) | | `audioSource` | `'mic' | 'display'` | `'mic'` | Local capture source (`mode: 'local'`) | | `audioDeviceId` | `string` | — | Specific input device for local mic capture | | `autoReconnect` | `boolean` | `false` | Reconnect the WebSocket with exponential backoff (1s→30s) after an unexpected drop | ## Data processing | Option | Type | Default | Meaning | | ------------ | ------------------- | ------- | --------------------------------------------------- | | `bands` | `10 | 20 | 40 | 80` | `80` | Number of frequency bands displayed | | `noiseFloor` | `number` | `0` | Cut magnitudes below this threshold (0–255) | | `smoothing` | `number` | `0` | Temporal smoothing (0 = none, 0.9 = heavy) | | `showPeaks` | `boolean` | `true` | Show falling peak indicators | | `peakDecay` | `number` | `0.997` | Peak fall speed (0.99 = slow, 0.9 = fast) | | `stereo` | `boolean` | `false` | Stereo mode: left channel top, right channel bottom | ## Appearance | Option | Type | Default | Meaning | | ------------------- | ------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------- | | `ledBars` | `boolean` | `false` | LED segment effect | | `ledShape` | `'segment' | 'meter'` | `'segment'` | `segment` = fixed-pixel gap lines; `meter` = short, wide segments like a classic LED meter | | `lumiBars` | `boolean` | `false` | Full-height bars whose brightness follows the level | | `radial` | `boolean` | `false` | Circular spectrum: angle = frequency, radius = level | | `radialInnerRadius` | `number` | `0.35` | Radial: inner hole radius as a fraction of outer radius (0–0.9) | | `barSpace` | `number` | `0.25` | Gap between bars as a fraction of bar width (0–0.9) | | `reflexRatio` | `number` | `0` | Mirrored reflection (0 = off). Linear mono: fraction of height (max 0.7). Radial: > 0 mirrors bars inward | | `reflexAlpha` | `number` | `0.25` | Reflection brightness (0–1) | | `glow` | `number` | `0` | Glow above the bar tops (0 = off, 1 = max) | | `rotation` | `0 | 90 | 180 | 270` | `0` | Rotate the whole visual clockwise, in degrees | | `gradient` | `GradientName | GradientStop[]` | `'classic'` | Bar colors: a preset name or custom stops — see [Gradients](https://www.fftvisualizer.com/guide/gradients) | | `gradientDirection` | `'vertical' | 'horizontal'` | `'vertical'` | Gradient axis | | `colorMode` | `'gradient' | 'bar-level'` | `'gradient'` | `bar-level` colors each whole bar by its current level | | `background` | `string` | `'#0a0a0a'` | Background behind/between bars. Any CSS color, incl. `'transparent'` / `rgba(…)` (fixed at mount) | | `debug` | `boolean` | `false` | Log connection/config diagnostics to the console | ::tip Both wrappers add a `showStats` prop for their built-in connection/fps overlay, and React adds `renderStats` , `className` and `style` — documented in [Vue props](https://www.fftvisualizer.com/vue/props) and [React props](https://www.fftvisualizer.com/react/props) . :: # Gradients Bar colors are rasterized into a 256×1 lookup texture, so any CSS color and native gradient interpolation work. Pass a preset name or an array of custom stops to the `gradient` option. ## Presets ::code-group ```ts [Core] new FFTVisualizer(canvas, { mode: 'local', gradient: 'sunset' }) ``` ```vue [Vue] ``` ```tsx [React] ``` :: Built-in presets (exported as `gradientPresets` / `gradientNames`): `classic` · `rainbow` · `blue` · `prism` · `orangered` · `steelblue` · `sunset` · `aurora` · `dusk` · `mono` ## Custom stops Any CSS color format works — hex, `rgb()`, `hsl()`, named colors: ::code-group ```ts [Core] new FFTVisualizer(canvas, { mode: 'local', gradient: [ { stop: 0, color: '#001233' }, { stop: 0.5, color: 'rgb(15, 155, 142)' }, { stop: 1, color: 'hsl(280, 95%, 75%)' } ] }) ``` ```vue [Vue] ``` ```tsx [React] ``` :: ## Direction & color mode - `gradientDirection: 'horizontal'` runs the gradient across the frequency axis instead of the level axis. - `colorMode: 'bar-level'` colors each whole bar by its current level (a VU-meter feel) rather than by position along the gradient axis. ## Helpers For building settings UIs or your own rendering, these are exported from every package: ::code-group ```ts [Core] import { gradientPresets, gradientNames, resolveGradientStops, buildGradientLUT, GRADIENT_LUT_SIZE, type GradientStop, type GradientName, type GradientInput } from '@fft-visualizer/core' ``` ```ts [Vue] import { gradientPresets, gradientNames, resolveGradientStops, buildGradientLUT, type GradientStop, type GradientName, type GradientInput } from '@fft-visualizer/vue' ``` ```ts [React] import { gradientPresets, gradientNames, resolveGradientStops, buildGradientLUT, type GradientStop, type GradientName, type GradientInput } from '@fft-visualizer/react' ``` :: The wrappers re-export these from the core for convenience. The `GRADIENT_LUT_SIZE` constant is **core-only** — import it from `@fft-visualizer/core` if you need it. # WebSocket protocol In `websocket` mode the visualizer connects to a server that has **already computed** the FFT and streams it as compact binary frames — so the browser only draws. This lets the FFT run on a separate device (a Raspberry Pi, a media server) feeding any number of browsers. ## The pre-computed-FFT protocol `mode: 'websocket'` expects a server that sends **pre-computed** FFT frames. **1. Config message (JSON), once on connect:** ```json { "type": "config", "mode": "fft", "bins": 80, "fps": 120 } ``` **2. Binary FFT frames, continuously:** - One `uint8` (0–255) per frequency bin — `bins` bytes per frame - 0 = silence, 255 = maximum amplitude - Typically 100 Hz – 18 kHz, exponentially spaced For best results the server should capture at 48 kHz+, apply a Hann/Hamming window, compute a 1024–2048-point FFT, map to exponentially-spaced bands, apply A-weighting, convert to dB, normalize to 0–255, and stream at 60–120 fps. ## Reference servers The repository's `backend-examples/` has servers that capture system audio, compute FFT, and stream it in this format: **Python** (pyalsaaudio + numpy, incl. a Raspberry Pi variant), **Node.js** (node-audiorecorder + fft.js), and **Rust** (cpal + rustfft). ## Two different WebSocket protocols Don't confuse the visualizer's `websocket` mode with the raw-PCM engines: | | Sends over the wire | FFT runs | Consumed by | | -------------------------------------------- | ---------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Pre-computed FFT** *(this page)* | `uint8` FFT magnitudes | On the server | `FFTVisualizer` `mode: 'websocket'` | | **Raw PCM** | Interleaved PCM audio | In the browser (WASM) | Core [`createWebSocketFft`](https://www.fftvisualizer.com/core/audio-engines) / Vue [`useWebSocketFft`](https://www.fftvisualizer.com/vue/composables) | The raw-PCM path streams audio and does the FFT client-side; feed its output into the visualizer with `mode: 'external'`. Its config message uses `"mode": "pcm"` with `sampleRate`, `bitDepth`, and `channels` fields instead of the `fft` config above. # AI tools These docs are machine-readable. There is an MCP server that any compatible assistant can connect to, and a set of plain-text files for tools that don't speak MCP. Both serve the same content as the pages you're reading — so an assistant using them cites what is actually documented rather than reconstructing an API from training data. ## MCP server ```text https://www.fftvisualizer.com/mcp ``` Streamable HTTP, no authentication, no API key. Nothing to install. ### Claude Code ```bash claude mcp add --transport http fft-visualizer https://www.fftvisualizer.com/mcp ``` ### Other clients Most MCP clients take a JSON block like this — the file it goes in and the exact field names vary, so check your client's own docs: ```json { "mcpServers": { "fft-visualizer": { "type": "http", "url": "https://www.fftvisualizer.com/mcp" } } } ``` ### Available tools - **`list-pages`** — every documentation page, with its title, path and description. - **`get-page`** — the full Markdown of one page, given a path from `list-pages`. A typical exchange is one `list-pages` call to find the right page, then one `get-page` to read it — so a question about, say, Nuxt integration is answered from [that page](https://www.fftvisualizer.com/vue/nuxt) verbatim. ## Without MCP For assistants that can fetch a URL but don't support MCP, the same content is available as plain text: - [`/llms.txt`](https://www.fftvisualizer.com/llms.txt){rel=""nofollow""} — an index: every page with a one-line summary and a link. - [`/llms-full.txt`](https://www.fftvisualizer.com/llms-full.txt){rel=""nofollow""} — the entire documentation in a single file, around 55 KB. - `/raw/.md` — any single page as Markdown, e.g. [`/raw/vue/nuxt.md`](https://www.fftvisualizer.com/raw/vue/nuxt.md){rel=""nofollow""}. Pasting `llms-full.txt` into a chat is the zero-setup option: it fits comfortably in a modern context window and covers the guide, the Core API, and both wrappers. ::note If an assistant suggests installing `fft-visualizer` unscoped, or calling an `init()` method on the class, it is answering from memory — the packages are [`@fft-visualizer/core`](https://www.fftvisualizer.com/core/installation) , [`@fft-visualizer/vue`](https://www.fftvisualizer.com/vue/installation) and [`@fft-visualizer/react`](https://www.fftvisualizer.com/react/installation) , and the canvas is a [constructor argument](https://www.fftvisualizer.com/core/api) . Connecting one of the sources above is the fix. :: # Installation `@fft-visualizer/core` is the framework-agnostic engine: a plain TypeScript class that renders onto a `` you provide. Use it directly in vanilla JS/TS, or as the foundation under a framework wrapper like [`@fft-visualizer/vue`](https://www.fftvisualizer.com/vue/installation). Prefer to poke at it first? The [live playground](https://demo.fftvisualizer.com/core/){rel=""nofollow""} runs this exact package with every option exposed. ## Install ::code-group ```bash [pnpm] pnpm add @fft-visualizer/core ``` ```bash [npm] npm install @fft-visualizer/core ``` ```bash [yarn] yarn add @fft-visualizer/core ``` :: ## Quick start The fastest way to see something is **local mode** — capture the microphone and visualize it entirely in the browser, no backend required. Give the class a `` and an options object: ```ts import { FFTVisualizer } from '@fft-visualizer/core' const canvas = document.querySelector('canvas')! const viz = new FFTVisualizer(canvas, { mode: 'local', bands: 40, gradient: 'aurora' }) // later, when you're done: viz.destroy() ``` The visualizer sizes itself to the canvas's **parent element** and observes it for layout changes, so give the container a height: ```html
``` Construction connects automatically (local mode prompts for microphone permission). See the [API reference](https://www.fftvisualizer.com/core/api) for `connect()` / `disconnect()`, live option updates, events, and teardown. ::note The Rust/WASM FFT processor is **lazy-loaded** — it's only fetched the first time you capture audio locally (or use [`createWebSocketFft`](https://www.fftvisualizer.com/core/audio-engines) ). Pure `websocket` / `external` usage never downloads it. :: ## Data modes `mode: 'local'` is one of three ways to feed the visualizer — see [Data modes](https://www.fftvisualizer.com/guide/data-modes) for `websocket` (pre-computed FFT from a server) and `external` (your own magnitudes). # API reference `@fft-visualizer/core` exports the imperative `FFTVisualizer` class plus a `parseCssColor` helper. All configuration uses the shared option set documented in [Options](https://www.fftvisualizer.com/guide/options). ## Constructor ```ts import { FFTVisualizer, type FFTVisualizerOptions } from '@fft-visualizer/core' const viz = new FFTVisualizer(canvas, options) ``` | Parameter | Type | Description | | --------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `canvas` | `HTMLCanvasElement` | The canvas to render into. The visualizer sizes to the canvas's **parent element** and observes it for resizes | | `options` | `FFTVisualizerOptions` *(optional)* | Any of the [shared options](https://www.fftvisualizer.com/guide/options) | Constructing the instance initializes WebGL and **connects immediately** using `options.mode` (local capture starts, a WebSocket opens, or external mode begins rendering). If WebGL fails to initialize, an `error` event is emitted instead. ## Getters | Getter | Type | Description | | --------------------- | -------------------- | -------------------------------------------------------------------------------- | | `isConnected` | `boolean` | Whether the data source is currently active | | `fps` | `number` | Frames processed in the last second | | `audioDevices` | `AudioDevice[]` | Known audio input devices (populated after `getAudioDevices()` or local capture) | | `activeAudioDeviceId` | `string | undefined` | The device ID currently in use for local mic capture | ## Methods | Method | Signature | Description | | ----------------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `connect` | `() => void` | Start the current `mode` (open the WebSocket, start capture, or begin external rendering). Called automatically by the constructor | | `disconnect` | `() => void` | Stop the data source and rendering | | `setOptions` | `(patch: Partial) => void` | Apply a partial options update at runtime (see below) | | `feedData` | `(data: Uint8Array, left?: Uint8Array, right?: Uint8Array) => void` | Feed FFT magnitudes (0–255) in `external` mode. The data is **copied**, so reusing one buffer per frame is safe. Pass `left` and `right` for stereo | | `refreshGradient` | `() => void` | Rebuild the gradient LUT after mutating custom stops **in place** (not needed when you assign a new `gradient`) | | `getAudioDevices` | `() => Promise` | Enumerate audio input devices (prompts for mic permission if needed) | | `destroy` | `() => void` | Disconnect, stop observing resize, remove all listeners, and free every WebGL resource | ### `setOptions(patch)` Only keys whose value actually changed take effect. Changing `mode` or `websocketUrl` reconnects; changing `bands` reallocates buffers; changing `background` or `gradient` reuploads the relevant texture. To reload a gradient whose stops you mutated in place, either pass a new `gradient` reference or call `refreshGradient()`. ```ts viz.setOptions({ ledBars: true, gradient: 'sunset', bands: 20 }) ``` ## Events Subscribe with `on()`, which returns an unsubscribe function; or remove a handler with `off()`. ```ts const stop = viz.on('frame', ({ data, left, right }) => { // data: Uint8Array — one 0–255 magnitude per bar (length = bands) // left / right: per-channel arrays in stereo mode, else null }) stop() // unsubscribe ``` | Event | Payload | Fires when | | -------------- | ------------ | ------------------------------------------------------------------------------- | | `connected` | — | The data source became active | | `disconnected` | — | The data source stopped | | `error` | `string` | A WebSocket error, capture failure, or WebGL init failure | | `frame` | `FrameEvent` | Once per processed audio frame, with the display bar magnitudes | | `audiostate` | — | Local-audio state changed (capture started/stopped, or the device list updated) | The `frame` event is a ready-made feed for external hardware (LED strips, flip-dot displays) — it hands you exactly what the shader draws, without re-implementing any FFT. It's listener-gated, so it costs nothing when no handler is attached. ```ts interface FrameEvent { data: Uint8Array // per-bar magnitudes (in stereo, the per-bar max of both channels) left: Uint8Array | null // per-channel in stereo, else null right: Uint8Array | null } ``` ## `parseCssColor(color)` ```ts import { parseCssColor } from '@fft-visualizer/core' parseCssColor('rgba(10, 10, 10, 0.5)') // => [r, g, b, a] normalized 0–1 ``` Parses any CSS color (including `rgba()` and `'transparent'`) into a normalized `[r, g, b, a]` tuple via a 1×1 canvas — the same routine the visualizer uses for the `background` option. ## Types `VisualizerMode` (`'websocket' | 'local' | 'external'`), `BandCount` (`10 | 20 | 40 | 80`), `FFTVisualizerOptions`, `FFTVisualizerEventMap`, and `FrameEvent` are all exported for typing your own code. # Audio engines The core ships the audio→FFT pipelines as standalone factories, so you can produce FFT magnitudes without the renderer — drive your own visuals, or feed the result into `FFTVisualizer` with `mode: 'external'`. These are exactly what the [Vue composables](https://www.fftvisualizer.com/vue/composables) wrap. All engines emit `Uint8Array` magnitudes (0–255 per bin) and share four options: `fftSize` (default `2048`), `bins` (default `80`), `startFreq` (default `100` Hz), and `endFreq` (default `18000` Hz). ## `createLocalAudio(options?)` Captures mic or system/tab audio and computes the FFT in-browser via the Rust WASM processor. ```ts import { createLocalAudio } from '@fft-visualizer/core' const audio = createLocalAudio({ bins: 80, onData: (data) => { // data: Uint8Array — fresh magnitudes each animation frame }, onStateChange: () => { // isActive / devices / activeDeviceId / sourceType changed } }) await audio.getDevices() // enumerate inputs (prompts for mic permission) await audio.start() // microphone (optional deviceId argument) await audio.startDisplay() // tab/system audio via screen sharing audio.stop() ``` The returned `LocalAudioEngine` exposes read-only `fftData`, `isActive`, `sourceType`, `devices`, and `activeDeviceId`, plus the `getDevices` / `start` / `startDisplay` / `stop` methods above. ## `createWebSocketFft(options?)` Connects to a WebSocket that streams **raw PCM** and computes the FFT client-side via WASM — distinct from `FFTVisualizer`'s `websocket` mode, which expects **pre-computed** FFT (see [WebSocket protocol](https://www.fftvisualizer.com/guide/websocket-protocol)). It produces mono, left, and right spectra. ```ts import { createWebSocketFft } from '@fft-visualizer/core' const engine = createWebSocketFft({ bins: 80, overlap: 0.5, // 0–0.75; more overlap = lower latency, ~2× FFT compute autoReconnect: true, onData: (mono, left, right) => { /* Uint8Array each */ } }) engine.connect('wss://your-server/pcm') engine.processSamples(float32Pcm) // or feed PCM manually engine.disconnect() engine.free() // tear down and free WASM processors ``` Read-only getters: `fftData`, `fftDataLeft`, `fftDataRight`, `isConnected`. ## `pcmToChannels(buffer, bitDepth, channels)` Decodes an interleaved integer-PCM `ArrayBuffer` (16/24/32-bit) into normalized Float32 channels — `{ mono, left, right }`, each in `[-1, 1)`. Mono input is duplicated to both channels; only the first two channels of multichannel input are used. This is the decoder `createWebSocketFft` uses internally. ## Raw WASM processor For full control, the Rust `FftProcessor` is exported from a dedicated subpath. It's kept out of the main entry so importing the visualizer never eagerly loads \~289 kB of WASM. ```ts import { FftProcessor } from '@fft-visualizer/core/wasm' const processor = new FftProcessor(fftSize, bins, startFreq, endFreq, sampleRate) const magnitudes = processor.process(float32TimeDomainSamples) // Uint8Array processor.free() ``` # Installation Prefer to poke at it first? The [live playground](https://demo.fftvisualizer.com/vue/){rel=""nofollow""} runs this exact package with every option exposed. ## Install ::code-group ```bash [pnpm] pnpm add @fft-visualizer/vue ``` ```bash [npm] npm install @fft-visualizer/vue ``` ```bash [yarn] yarn add @fft-visualizer/vue ``` :: Import the stylesheet once, anywhere in your app — it sizes the canvas and styles the stats overlay: ```ts import '@fft-visualizer/vue/style.css' ``` ## Quick start The fastest way to see something is **local mode** — capture the microphone and visualize it entirely in the browser, no backend required: ```vue ``` The browser prompts for microphone permission on mount. To capture tab or system audio instead of the mic, set `audio-source="display"`. ::note The component fills its container, so the wrapper needs a defined height. :: ## Nuxt The component is SSR-safe (all browser access is deferred to `onMounted`), but it renders nothing meaningful on the server — wrap it in ``: ```vue ``` See [Nuxt](https://www.fftvisualizer.com/vue/nuxt) for the stylesheet, layout shift, and using the core directly. ::note `mode="local"` is one of three [data modes](https://www.fftvisualizer.com/guide/data-modes) . Every prop maps to a shared [option](https://www.fftvisualizer.com/guide/options) ; see [Props, events & methods](https://www.fftvisualizer.com/vue/props) for the Vue-specific API. :: # Props, events & methods ## Props The `` component accepts **every** [shared option](https://www.fftvisualizer.com/guide/options) as a prop, one-to-one, in `camelCase` or `kebab-case`: ```vue ``` See [Options](https://www.fftvisualizer.com/guide/options) for the full list of names, types, defaults, and meanings. Object/array props (`data`, `dataLeft`, `dataRight`, custom `gradient` stops) are bound with `:`, e.g. `:gradient="[{ stop: 0, color: '#001233' }, …]"`. ### Vue-only prop One prop exists only on the Vue wrapper, for its built-in overlay: | Prop | Type | Default | Description | | ----------- | --------- | ------- | ------------------------------------------------------------------------ | | `showStats` | `boolean` | `true` | Show the small connection/fps overlay (overridable via the `stats` slot) | ## Events | Event | Payload | Description | | -------------- | --------------------- | ------------------------------------------------------------- | | `connected` | — | Data source became active | | `disconnected` | — | Data source stopped | | `error` | `string` | Error message (WS error, capture failure, WebGL init failure) | | `frame` | `(data, left, right)` | The display bar magnitudes, once per processed audio frame | ### The `frame` event Fires once per processed audio frame with the same 0–255 bar values the shader draws — a ready-made feed for external hardware (LED strips, flip-dot displays, …) without re-implementing any FFT. Listener-gated, so it costs nothing when unused. ```vue ``` ```ts // data: Uint8Array — one 0–255 magnitude per bar (length = `bands`) // left / right: per-channel arrays in stereo mode, else null // (in stereo, `data` is the per-bar max of both channels) function onFrame(data, left, right) { const rows = 14 // e.g. dots per column on a flip-dot panel data.forEach((v, col) => { const lit = Math.round((v / 255) * rows) // light dots 0..lit-1 in column `col` }) } ``` ## Exposed methods Access via a template ref: ```vue ``` ## Slots | Slot | Props | Description | | ------- | --------------------------- | ----------------------------------------------------------------------------------------- | | `stats` | `{ connected, bands, fps }` | Replace the default corner overlay with your own (only rendered when `showStats` is true) | # Composables The data pipelines are also available as Vue composables, if you want to drive your own rendering or combine them with `mode="external"`. They wrap the framework-agnostic [core audio engines](https://www.fftvisualizer.com/core/audio-engines) and surface their state as refs. ## `useLocalAudio(options?)` Captures mic or display audio and runs the WASM FFT. ```ts const { fftData, // Ref — magnitudes 0–255 isActive, sourceType, devices, activeDeviceId, getDevices, // () => Promise start, // (deviceId?) => Promise — microphone startDisplay, // () => Promise — tab/system audio stop } = useLocalAudio({ fftSize: 2048, bins: 80, startFreq: 100, endFreq: 18000 }) ``` ## `useWebSocketFft(options?)` Connects to a WebSocket that streams **raw PCM** and computes the FFT in-browser via WASM (distinct from the component's `websocket` mode, which expects **pre-computed** FFT — see [WebSocket protocol](https://www.fftvisualizer.com/guide/websocket-protocol)). Feed its `fftData` into the component with `mode="external"`. ```ts const { fftData, fftDataLeft, fftDataRight, isConnected, connect, // (url) => void disconnect, processSamples // (Float32Array) => void — feed PCM manually } = useWebSocketFft({ fftSize: 2048, bins: 80, overlap: 0.5, autoReconnect: true }) ``` The raw WASM FFT processor is also exported from `@fft-visualizer/vue/wasm` for direct use (re-exported from [`@fft-visualizer/core/wasm`](https://www.fftvisualizer.com/core/audio-engines#raw-wasm-processor)). # Nuxt The Vue component works in Nuxt with no module and no plugin. It is SSR-safe — every browser API it touches (`window`, `navigator`, WebGL, `AudioContext`) is behind `onMounted` — so importing it never breaks a server render. But it draws nothing on the server, so it belongs inside [``](https://nuxt.com/docs/api/components/client-only){rel=""nofollow""}. Prefer to see it running first? The [Nuxt playground](https://demo.fftvisualizer.com/nuxt/){rel=""nofollow""} is this exact setup on a prerendered page. ## Install ```bash [pnpm] pnpm add @fft-visualizer/vue ``` Then register the stylesheet in `nuxt.config.ts` — it sizes the canvas and styles the stats overlay, and the component renders unstyled without it: ```ts [nuxt.config.ts] export default defineNuxtConfig({ css: ['@fft-visualizer/vue/style.css'], }) ``` ## Quick start ```vue [app.vue] ``` `mode="local"` captures the microphone and computes the FFT in the browser, so this needs no backend. The browser prompts for permission on mount. ::note Give the wrapper a height *outside* `` , as above. If the sized element only exists after hydration, the page reflows the moment the visualizer appears. :: ## Avoiding layout shift `` renders nothing on the server, which leaves a gap until hydration. Its `#fallback` slot fills that gap with server-rendered markup: ```vue ``` Hydration replaces the entire `` subtree, so anything you want to survive that swap — a heading, a toolbar — belongs outside it, or in *both* branches. ## Using the core directly If you skip the component and drive [`@fft-visualizer/core`](https://www.fftvisualizer.com/core/api) yourself, construct it in `onMounted` and guard any module-scope browser access with `import.meta.client`: ```vue ``` ::warning The canvas is a **constructor argument** , not a later `init()` call — the class has no such method, and `connect()` runs automatically on construction. See the [API reference](https://www.fftvisualizer.com/core/api) for the full surface. :: # Installation Prefer to poke at it first? The [live playground](https://demo.fftvisualizer.com/react/){rel=""nofollow""} runs this exact package with every option exposed. ## Install ::code-group ```bash [pnpm] pnpm add @fft-visualizer/react ``` ```bash [npm] npm install @fft-visualizer/react ``` ```bash [yarn] yarn add @fft-visualizer/react ``` :: React 18 and 19 are both supported, and React is the only peer dependency. Import the stylesheet once, anywhere in your app — it sizes the canvas and styles the stats overlay: ```ts import '@fft-visualizer/react/style.css' ``` ## Quick start The fastest way to see something is **local mode** — capture the microphone and visualize it entirely in the browser, no backend required: ```tsx import { FFTVisualizer } from '@fft-visualizer/react' import '@fft-visualizer/react/style.css' export function Viz() { // The component fills its container — give it a height return (
) } ``` The browser prompts for microphone permission on mount. To capture tab or system audio instead of the mic, set `audioSource="display"`. ::note The component fills its container, so the wrapper needs a defined height. :: ## Next.js & React Server Components The component renders on the server (every browser API is touched inside `useEffect`), but it needs the browser to do anything — so it belongs in a client component: ```tsx 'use client' import { FFTVisualizer } from '@fft-visualizer/react' ``` ::note `mode="local"` is one of three [data modes](https://www.fftvisualizer.com/guide/data-modes) . Every prop maps to a shared [option](https://www.fftvisualizer.com/guide/options) ; see [Props, callbacks & ref](https://www.fftvisualizer.com/react/props) for the React-specific API. :: # Props, callbacks & ref ## Props The `` component accepts **every** [shared option](https://www.fftvisualizer.com/guide/options) as a prop, one-to-one, in `camelCase`: ```tsx ``` See [Options](https://www.fftvisualizer.com/guide/options) for the full list of names, types, defaults, and meanings. Unset props keep the core's default, so you only pass what you want to change. Custom `gradient` stops are compared **by value**, so an inline array is fine — the gradient texture is only rebuilt when the colors actually change: ```tsx ``` ### React-only props Four props exist only on the React wrapper: | Prop | Type | Default | Description | | ------------- | ------------------------------------------------- | ------- | ----------------------------------------------------------------------------------- | | `showStats` | `boolean` | `true` | Show the small connection/fps overlay in the corner | | `renderStats` | `(state: { connected, bands, fps }) => ReactNode` | — | Replace the overlay's content with your own (only rendered when `showStats`) | | `className` | `string` | — | Extra class names on the root element (the built-in `fft-visualizer` class is kept) | | `style` | `CSSProperties` | — | Extra styles on the root element (merged over the `background` option) | ## Callbacks | Prop | Payload | Description | | ---------------- | --------------------- | ------------------------------------------------------------- | | `onConnected` | — | Data source became active | | `onDisconnected` | — | Data source stopped | | `onError` | `string` | Error message (WS error, capture failure, WebGL init failure) | | `onFrame` | `(data, left, right)` | The display bar magnitudes, once per processed audio frame | Callbacks are read through a ref, so inline arrow functions are fine — changing one never resubscribes or restarts the visualizer. ### `onFrame` Fires once per processed audio frame with the same 0–255 bar values the shader draws — a ready-made feed for external hardware (LED strips, flip-dot displays, …) without re-implementing any FFT. Listener-gated, so it costs nothing when unused. ```tsx // data: Uint8Array — one 0–255 magnitude per bar (length = `bands`) // left / right: per-channel arrays in stereo mode, else null // (in stereo, `data` is the per-bar max of both channels) function onFrame(data: Uint8Array, left: Uint8Array | null, right: Uint8Array | null) { const rows = 14 // e.g. dots per column on a flip-dot panel data.forEach((v, col) => { const lit = Math.round((v / 255) * rows) // light dots 0..lit-1 in column `col` }) } ``` ## Imperative handle Attach a ref for manual control: ```tsx import { useRef } from 'react' import { FFTVisualizer, type FFTVisualizerHandle } from '@fft-visualizer/react' const viz = useRef(null) // Manual connection control viz.current?.connect() viz.current?.disconnect() viz.current?.isConnected // boolean // Feed FFT frames imperatively (mode="external") — copies the data, so it works // even when you reuse one buffer each frame, and avoids a render per frame viz.current?.feedData(mono) // Uint8Array viz.current?.feedData(mono, left, right) // stereo // Local-audio device management await viz.current?.getAudioDevices() // Promise (prompts for mic permission) viz.current?.audioDevices // AudioDevice[] viz.current?.activeAudioDeviceId // string | undefined ``` ## Standalone audio engines There are no React-specific hooks: the data pipelines are framework-agnostic and live in the core package. Use [`createLocalAudio` / `createWebSocketFft`](https://www.fftvisualizer.com/core/audio-engines) directly — with `mode="external"`, or to drive your own rendering. Both are callback-driven, which keeps per-frame data out of React state. ```ts import { createLocalAudio, createWebSocketFft } from '@fft-visualizer/core' ``` The raw WASM FFT processor is also re-exported from `@fft-visualizer/react/wasm` (from [`@fft-visualizer/core/wasm`](https://www.fftvisualizer.com/core/audio-engines#raw-wasm-processor)). # WebGL Audio Spectrum Analyzer for the Web ::u-page-hero --- orientation: horizontal --- :::div{.hero-visual} ![FFT Visualizer radial spectrum](https://www.fftvisualizer.com/hero.png){height="926" width="1034"} ::: #title Real-time audio spectrum, on the GPU #description A high-performance, WebGL-based audio spectrum analyzer and FFT visualizer for the web — a framework-agnostic TypeScript core with first-class Vue 3 and React components. Visualize the microphone, tab/system audio, a WebSocket stream, or your own data — the entire frame drawn by a single fragment shader. #links :::u-button --- color: neutral size: xl to: https://www.fftvisualizer.com/guide/introduction trailing-icon: i-lucide-arrow-right --- Get started ::: :::u-button --- color: neutral icon: i-lucide-play size: xl target: _blank to: https://demo.fftvisualizer.com/ variant: subtle --- Live demo ::: :::u-button --- color: neutral icon: i-simple-icons-github size: xl to: https://github.com/harmonics-audio/fft-visualizer variant: outline --- View on GitHub ::: :: ::u-container :demo-player :: ::u-page-section #title Why FFT Visualizer #features :::u-page-feature --- icon: i-lucide-cpu --- #title Single-draw-call WebGL #description Every bar, LED segment, gradient, glow and reflection is rendered by one fragment shader — smooth at 120fps with 80 bands in stereo. ::: :::u-page-feature --- icon: i-lucide-mic --- #title Three data sources #description Capture audio locally (mic or tab/system), stream pre-computed FFT over WebSocket, or feed your own magnitudes via props. ::: :::u-page-feature --- icon: i-lucide-waves --- #title In-browser FFT (Rust/WASM) #description Optional WebAssembly FFT processor, lazy-loaded only when you capture audio locally. Zero backend required. ::: :::u-page-feature --- icon: i-lucide-palette --- #title Rich visual modes #description LED segments, radial layout, stereo, mirrored reflection, glow, rotation, and 10 gradient presets or your own CSS-color stops. ::: :::u-page-feature --- icon: i-lucide-package --- #title Framework-agnostic core #description A vanilla-TypeScript engine with thin wrappers for Vue and React. Rendering uses native WebGL: no charting library, no canvas 2D. ::: :::u-page-feature --- icon: i-lucide-square-dashed --- #title Transparent & themeable #description Any background color, including a fully transparent canvas that blends into your page. SSR-safe for Nuxt. ::: ::