Audio Visualization

Learn how to stream real-time wave and FFT audio data

flutter_soloud provides high-performance, real-time audio visualization powered by 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

ParameterTypeDefaultDescription
enabledbool(required)Enables or disables the analyzer engine and callback stream.
windowSizeint256FFT and Wave window size. Must be a power of two from 128 to 8192 (128, 256, 512, 1024, 2048, 4096, 8192).
kindVisualizationKindVisualizationKind.waveAndFftWhat data to compute: wave only, fft only, or waveAndFft.
channelintVisualizationChannel.mergedChannel 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 &lt; count; i++) {
      // Waveform (top half)
      if (i &lt; 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 &lt; 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).