---
title: Audio Visualization
description: Learn how to stream real-time wave and FFT audio data
---

*flutter_soloud* provides high-performance, real-time audio visualization powered by [PFFFT](https://github.com/martonparlagh/pffft) and miniaudio. It delivers audio waveforms and frequency spectra directly to Dart through a reactive `Stream<AudioVisualizationData>`.

## Setup & Enabling Visualization

Visualization is enabled via `SoLoud.instance.setVisualizationEnabled(...)`.

```dart
// 1. Initialize the player engine
await SoLoud.instance.init(
  bufferSize: 1024,
  channels: Channels.stereo,
);

// 2. Enable visualization (defaults to windowSize: 256, kind: waveAndFft, channel: merged)
SoLoud.instance.setVisualizationEnabled(
  true,
  windowSize: 256,
  kind: VisualizationKind.waveAndFft,
  channel: VisualizationChannel.merged,
);
```

### Configuration Options

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `enabled` | `bool` | *(required)* | Enables or disables the analyzer engine and callback stream. |
| `windowSize` | `int` | `256` | FFT and Wave window size. Must be a power of two from **128** to **8192** (128, 256, 512, 1024, 2048, 4096, 8192). |
| `kind` | `VisualizationKind` | `VisualizationKind.waveAndFft` | What data to compute: `wave` only, `fft` only, or `waveAndFft`. |
| `channel` | `int` | `VisualizationChannel.merged` | Channel selection: `VisualizationChannel.merged` (-1), `VisualizationChannel.all` (-2), or a specific 0-based channel index (`0`, `1`, ...). |

## AudioVisualizationData

Every time new audio data is processed by the mixer, an `AudioVisualizationData` packet is emitted on `SoLoud.instance.audioVisualizationEvents`.

### Properties

- `channelCount`: The number of channels included in this data packet.
- `wave`: `List<Float32List>` containing waveform samples per channel (normalized in range `[-1.0, 1.0]`, length equals `windowSize`).
- `fft`: `List<Float32List>` containing FFT magnitude bins per channel (normalized in range `[0.0, 1.0]`, length equals `windowSize / 2`).
- `waveData` / `fftData`: Convenience getters for single-channel / merged audio data (`wave.first` / `fft.first`).

## Basic Example

Here is a complete widget listening to `audioVisualizationEvents` and rendering both waveform and frequency spectrum using a `CustomPainter`:

```dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';

class AudioVisualizerWidget extends StatefulWidget {
  const AudioVisualizerWidget({super.key});

  @override
  State<AudioVisualizerWidget> createState() => _AudioVisualizerWidgetState();
}

class _AudioVisualizerWidgetState extends State<AudioVisualizerWidget> {
  StreamSubscription<AudioVisualizationData>? _subscription;
  AudioVisualizationData? _latestData;

  @override
  void initState() {
    super.initState();
    _subscription = SoLoud.instance.audioVisualizationEvents.listen((data) {
      if (mounted) {
        setState(() {
          _latestData = data;
        });
      }
    });
  }

  @override
  void dispose() {
    _subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: double.infinity,
      height: 200,
      child: CustomPaint(
        painter: SpectrumWavePainter(data: _latestData),
      ),
    );
  }
}

class SpectrumWavePainter extends CustomPainter {
  const SpectrumWavePainter({required this.data});

  final AudioVisualizationData? data;

  @override
  void paint(Canvas canvas, Size size) {
    final vis = data;
    if (vis == null) return;

    final wave = vis.waveData ?? Float32List(0);
    final fft = vis.fftData ?? Float32List(0);

    final count = wave.isNotEmpty ? wave.length : fft.length;
    if (count == 0) return;

    final barWidth = size.width / count;
    final paint = Paint()
      ..strokeWidth = barWidth * 0.8
      ..color = Colors.cyanAccent;

    for (var i = 0; i < count; i++) {
      // Waveform (top half)
      if (i < wave.length) {
        final sample = wave[i];
        final waveHeight = size.height * sample * 0.4;
        canvas.drawRect(
          Rect.fromLTWH(
            i * barWidth,
            (size.height * 0.25) - (waveHeight / 2),
            barWidth * 0.8,
            waveHeight.abs().clamp(1.0, size.height * 0.5),
          ),
          paint,
        );
      }

      // FFT Spectrum (bottom half)
      if (i < fft.length) {
        final mag = fft[i];
        final barHeight = mag * (size.height * 0.45);
        canvas.drawRect(
          Rect.fromLTWH(
            i * barWidth,
            size.height - barHeight,
            barWidth * 0.8,
            barHeight,
          ),
          paint,
        );
      }
    }
  }

  @override
  bool shouldRepaint(covariant SpectrumWavePainter oldDelegate) {
    return oldDelegate.data != data;
  }
}
```

## Channel Modes

### 1. Merged Mono (Default)
Downmixes all active mixer output channels into a single mono channel using `miniaudio`'s channel matrix converter.

```dart
SoLoud.instance.setVisualizationEnabled(
  true,
  channel: VisualizationChannel.merged,
);
```

### 2. Multi-Channel (All Channels)
Provides independent waveform and FFT data for each speaker channel (e.g. Left and Right for stereo).

```dart
SoLoud.instance.setVisualizationEnabled(
  true,
  channel: VisualizationChannel.all,
);

// Access in stream callback:
SoLoud.instance.audioVisualizationEvents.listen((data) {
  final leftWave = data.wave[0];
  final rightWave = data.wave[1];
  final leftFft = data.fft[0];
  final rightFft = data.fft[1];
});
```

### 3. Specific Single Channel
Extracts data from a single 0-indexed channel (e.g. `0` for Left, `1` for Right):

```dart
SoLoud.instance.setVisualizationEnabled(
  true,
  channel: 0, // Left channel only
);
```

## FFT Smoothing

To create smooth, aesthetically pleasing spectrum meters without erratic flickering, use `setFftSmoothing`:

```dart
// Smooth value between 0.0 (no smoothing) and 1.0 (maximum smoothing)
SoLoud.instance.setFftSmoothing(0.85);
```

The smoothing filter interpolates declining frequency bands using an exponential decay model:
$$\text{band}_{t} = \text{smooth} \times \text{band}_{t-1} + (1 - \text{smooth}) \times \text{band}_t$$

## Window Sizes and Frequency Resolution

Window sizes must be powers of two between 128 and 8192:

- **128**: 64 FFT bins. Ultra-low latency, coarse frequency resolution.
- **256** *(default)*: 128 FFT bins. Optimal balance for UI animations and 60fps visuals.
- **512**: 256 FFT bins. Great frequency detail for music visualizers.
- **1024 / 2048**: 512 / 1024 FFT bins. High-resolution spectrum analysis.
- **4096 / 8192**: Deep low-frequency resolution for studio tools.

## Zero-Copy Memory Access (FFI)

For performance-critical visualization in C/C++, Flutter shaders, or custom native renderers, `AudioVisualizationData` exposes direct C pointers without copying memory buffers:

```dart
final Pointer<Float>? wavePtr = data.wavePointer;
final Pointer<Float>? fftPtr = data.fftPointer;
```

On Flutter Web, `Float32List` views are mapped directly to the WebAssembly linear memory heap (`wasmHeapF32`).
