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>.
Visualization is enabled via SoLoud.instance.setVisualizationEnabled(...).
// 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,
);| 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, ...). |
Every time new audio data is processed by the mixer, an AudioVisualizationData packet is emitted on SoLoud.instance.audioVisualizationEvents.
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 equalswindowSize).fft:List<Float32List>containing FFT magnitude bins per channel (normalized in range[0.0, 1.0], length equalswindowSize / 2).waveData/fftData: Convenience getters for single-channel / merged audio data (wave.first/fft.first).
Here is a complete widget listening to audioVisualizationEvents and rendering both waveform and frequency spectrum using a CustomPainter:
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;
}
}Downmixes all active mixer output channels into a single mono channel using miniaudio's channel matrix converter.
SoLoud.instance.setVisualizationEnabled(
true,
channel: VisualizationChannel.merged,
);Provides independent waveform and FFT data for each speaker channel (e.g. Left and Right for stereo).
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];
});Extracts data from a single 0-indexed channel (e.g. 0 for Left, 1 for Right):
SoLoud.instance.setVisualizationEnabled(
true,
channel: 0, // Left channel only
);To create smooth, aesthetically pleasing spectrum meters without erratic flickering, use setFftSmoothing:
// 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 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.
For performance-critical visualization in C/C++, Flutter shaders, or custom native renderers, AudioVisualizationData exposes direct C pointers without copying memory buffers:
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).