Visualization in flutter_soloud
flutter_soloud has two unrelated ways to get drawable sample data out of the SoLoud engine:
- Live visualization — the engine emits
AudioVisualizationData packets (waveform and/or FFT magnitudes of the mixed output) on a broadcast stream, at mixer rate. For real-time spectrum/waveform widgets.
- Offline waveform reading —
readSamplesFromFile / readSamplesFromMem decode an audio file in a background isolate and return a fixed number of equally-spaced samples. For static waveform previews (SoundCloud-style).
Pick the right one. The live stream cannot give you the waveform of a not-yet-played file; the offline readers cannot follow playback.
Minimal example (live FFT + wave)
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_soloud/flutter_soloud.dart';
class Visualizer extends StatefulWidget {
const Visualizer({super.key});
@override
State<Visualizer> createState() => _VisualizerState();
}
class _VisualizerState extends State<Visualizer> {
StreamSubscription<AudioVisualizationData>? _sub;
AudioVisualizationData? _data;
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
await SoLoud.instance.init(); // must complete first
SoLoud.instance.setVisualizationEnabled(
true,
windowSize: 256,
kind: VisualizationKind.waveAndFft,
channel: VisualizationChannel.merged,
);
SoLoud.instance.setFftSmoothing(0.8);
_sub = SoLoud.instance.audioVisualizationEvents.listen((data) {
if (mounted) setState(() => _data = data);
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final fft = _data?.fftData; // Float32List? of windowSize/2 magnitudes
if (fft == null) return const SizedBox(height: 100);
return CustomPaint(
size: const Size(double.infinity, 100),
painter: BarsPainter(fft),
);
}
}
class BarsPainter extends CustomPainter {
BarsPainter(this.fft);
final Float32List fft;
@override
void paint(Canvas canvas, Size size) {
final barWidth = size.width / fft.length;
final paint = Paint()..strokeWidth = barWidth * 0.8;
for (var i = 0; i < fft.length; i++) {
final h = size.height * fft[i].clamp(0.0, 1.0);
canvas.drawLine(
Offset(i * barWidth + barWidth / 2, size.height),
Offset(i * barWidth + barWidth / 2, size.height - h),
paint,
);
}
}
@override
bool shouldRepaint(BarsPainter oldDelegate) => true; // data changes every event
}
Minimal example (offline waveform)
import 'package:flutter/foundation.dart';
import 'package:flutter_soloud/flutter_soloud.dart';
// No SoLoud.instance.init() needed — the readers run their own decoder
// inside a `compute` isolate.
Future<Float32List> waveformBars(String path, Uint8List bytes, int bars) {
if (kIsWeb) {
// readSamplesFromFile does not exist on Web; feed it the bytes.
return SoLoud.instance.readSamplesFromMem(bytes, bars, average: true);
}
return SoLoud.instance.readSamplesFromFile(path, bars, average: true);
}
A common choice for bars is the widget's pixel width (MediaQuery.sizeOf(context).width.toInt() * 4 in the plugin's example). Draw each value as a vertical line centered on the middle (see example/lib/wave_data/wave_data.dart).
The API shape
All on SoLoud.instance (singleton), imported from package:flutter_soloud/flutter_soloud.dart.
Live
void setVisualizationEnabled(bool enabled, {int windowSize = 256, VisualizationKind kind = VisualizationKind.waveAndFft, int channel = VisualizationChannel.merged}) — turns the analyzer on/off. Must be called after init(); throws SoLoudNotInitializedException otherwise, SoLoudCppException on bad params.
Stream<AudioVisualizationData> get audioVisualizationEvents — broadcast stream; emits one packet per mixer buffer while enabled.
AudioVisualizationData — channelCount, wave (List<Float32List>, one per channel, length windowSize, values [-1.0, 1.0]), fft (List<Float32List>, one per channel, length windowSize / 2, magnitudes [0.0, 1.0]). Either list is empty if its kind is disabled. waveData / fftData are nullable convenience getters for wave.first / fft.first.
VisualizationKind — real enum: wave, fft, waveAndFft.
VisualizationChannel — not an enum, an abstract final class of int constants: merged = -1 (downmix to mono), all = -2 (per-channel lists), or pass any 0-based channel index (0 = left). There is no VisualizationChannel.values; don't switch over it.
void setFftSmoothing(double smooth) — exponential decay on falling FFT bins, 0.0–1.0. Rise is never smoothed.
double getApproximateVolume(int channel) — instantaneous overall output volume per output channel (speaker). Poll it on a Timer for a VU meter. Returns 0 for invalid channels.
Offline
Future<Float32List> readSamplesFromFile(String completeFileName, int numSamplesNeeded, {double startTime = 0, double endTime = -1, bool average = false}) — native platforms only.
Future<Float32List> readSamplesFromMem(Uint8List buffer, int numSamplesNeeded, {double startTime = 0, double endTime = -1, bool average = false}) — everywhere; buffer is the encoded file bytes (mp3/wav/flac/ogg, from assets, File.readAsBytes(), or a network fetch).
- Times are in seconds;
endTime: -1 means "to end of file". average: true makes each returned value the mean of its time bucket instead of a single point sample — smoother waveforms, negligible cost.
Divergences from audioplayers / just_audio / web Audio API assumptions
- No
AudioPlayer-style per-sound analyser. Live visualization taps the whole mixed engine output (all voices, post-filter), not an individual track.
- There is no
onPositionChanged-style callback to hook FFT onto; you enable the analyzer once and consume a stream.
getApproximateVolume is per output channel, not per voice — there is no built-in per-sound level meter.
- The offline readers don't require
init() and don't need the sound loaded via loadMem/loadAsset first — they decode independently (asset bytes via rootBundle.load are fine).
Traps
- Silent stream.
audioVisualizationEvents emits nothing until setVisualizationEnabled(true) is called, and calling it before init() completes throws. If your painter stays black, check enablement and init order first.
windowSize must be a power of two in 128–8192. Anything else fails with SoLoudCppException, not an assertion. FFT bin count is windowSize / 2, wave length is windowSize — don't assume they're equal.
- Empty lists by kind. With
kind: VisualizationKind.fft, data.wave is empty and data.waveData is null — null-check the convenience getters.
- Every event is a fresh copy. The binding copies native memory into new
Float32Lists per packet (Float32List.fromList), so retaining an event is safe and cheap to reason about — but don't hold a queue of them unbounded. Note: older docs mention wavePointer/fftPointer getters on AudioVisualizationData; they do not exist in this version of the package.
- Event rate is tied to
bufferSize, not frames. With the default bufferSize: 2048 at 44.1 kHz you get 21 packets/s; the example uses init(bufferSize: 1024) (43/s) for smoother visuals. Don't do heavy layout per event; a CustomPainter repaint is fine.
deinit() disables visualization (and deinit() is called when init() re-initializes). Re-enable after re-init.
readSamplesFromFile throws/is unavailable on Web — gate on kIsWeb and use readSamplesFromMem with the file bytes. On Web, readSamplesFromMem runs synchronously (no real isolate) and can jank the UI for large files.
- Returned length ≠
numSamplesNeeded. The list is shorter if endTime overshoots the file duration. Size your painter from data.length, never from the requested count.
- Asserts on time range:
startTime >= 0 and endTime > startTime (unless -1). Passing a Duration won't compile — convert with duration.inMilliseconds / 1000.
- Per-channel indexing is by mixer output channels. With
channel: VisualizationChannel.all, data.wave[0] is left, [1] is right — the number of entries equals the engine's Channels setting at init(), not the file's channel count.
More depth
- references/live_painter_recipe.md — full per-channel wave + FFT
CustomPainter widget, adapted from the plugin example.
- references/offline_waveform.md — full
readSamplesFromMem flow with file picking, averaging, and the waveform painter.
- Live demo:
example/lib/audio_data/audio_data.dart (+ data_widget.dart). Offline demo: example/lib/wave_data/wave_data.dart.
Keeping this skill current
This skill ships inside the flutter_soloud package, so upgrading flutter_soloud can carry a newer revision of it than the copy installed in the project. To check, run:
dart run flutter_soloud:skills --check
It reports the installed and bundled skill versions and exits non-zero when an update is available. Offer to update with dart run flutter_soloud:skills (which touches only the skills, never pubspec or build files).
1---2name: flutter-soloud-visualization3description: Teaches flutter_soloud's two visualization paths — live FFT/wave data via setVisualizationEnabled + the audioVisualizationEvents stream, and offline waveform extraction via readSamplesFromFile/readSamplesFromMem (plus getApproximateVolume for VU meters). Use when the user asks for a spectrum/waveform visualizer, audio-reactive UI, VU/level meter, or a static waveform preview of an audio file.4---56# Visualization in flutter_soloud78flutter_soloud has two unrelated ways to get drawable sample data out of the SoLoud engine:9101. **Live visualization** — the engine emits `AudioVisualizationData` packets (waveform and/or FFT magnitudes of the *mixed output*) on a broadcast stream, at mixer rate. For real-time spectrum/waveform widgets.112. **Offline waveform reading** — `readSamplesFromFile` / `readSamplesFromMem` decode an audio file in a background isolate and return a fixed number of equally-spaced samples. For static waveform previews (SoundCloud-style).1213Pick the right one. The live stream cannot give you the waveform of a not-yet-played file; the offline readers cannot follow playback.1415## Minimal example (live FFT + wave)1617```dart18import 'dart:async';1920import 'package:flutter/material.dart';21import 'package:flutter_soloud/flutter_soloud.dart';2223class Visualizer extends StatefulWidget {24 const Visualizer({super.key});25 @override26 State<Visualizer> createState() => _VisualizerState();27}2829class _VisualizerState extends State<Visualizer> {30 StreamSubscription<AudioVisualizationData>? _sub;31 AudioVisualizationData? _data;3233 @override34 void initState() {35 super.initState();36 _init();37 }3839 Future<void> _init() async {40 await SoLoud.instance.init(); // must complete first41 SoLoud.instance.setVisualizationEnabled(42 true,43 windowSize: 256,44 kind: VisualizationKind.waveAndFft,45 channel: VisualizationChannel.merged,46 );47 SoLoud.instance.setFftSmoothing(0.8);48 _sub = SoLoud.instance.audioVisualizationEvents.listen((data) {49 if (mounted) setState(() => _data = data);50 });51 }5253 @override54 void dispose() {55 _sub?.cancel();56 super.dispose();57 }5859 @override60 Widget build(BuildContext context) {61 final fft = _data?.fftData; // Float32List? of windowSize/2 magnitudes62 if (fft == null) return const SizedBox(height: 100);63 return CustomPaint(64 size: const Size(double.infinity, 100),65 painter: BarsPainter(fft),66 );67 }68}6970class BarsPainter extends CustomPainter {71 BarsPainter(this.fft);72 final Float32List fft;7374 @override75 void paint(Canvas canvas, Size size) {76 final barWidth = size.width / fft.length;77 final paint = Paint()..strokeWidth = barWidth * 0.8;78 for (var i = 0; i < fft.length; i++) {79 final h = size.height * fft[i].clamp(0.0, 1.0);80 canvas.drawLine(81 Offset(i * barWidth + barWidth / 2, size.height),82 Offset(i * barWidth + barWidth / 2, size.height - h),83 paint,84 );85 }86 }8788 @override89 bool shouldRepaint(BarsPainter oldDelegate) => true; // data changes every event90}91```9293## Minimal example (offline waveform)9495```dart96import 'package:flutter/foundation.dart';97import 'package:flutter_soloud/flutter_soloud.dart';9899// No SoLoud.instance.init() needed — the readers run their own decoder100// inside a `compute` isolate.101Future<Float32List> waveformBars(String path, Uint8List bytes, int bars) {102 if (kIsWeb) {103 // readSamplesFromFile does not exist on Web; feed it the bytes.104 return SoLoud.instance.readSamplesFromMem(bytes, bars, average: true);105 }106 return SoLoud.instance.readSamplesFromFile(path, bars, average: true);107}108```109110A common choice for `bars` is the widget's pixel width (`MediaQuery.sizeOf(context).width.toInt() * 4` in the plugin's example). Draw each value as a vertical line centered on the middle (see `example/lib/wave_data/wave_data.dart`).111112## The API shape113114All on `SoLoud.instance` (singleton), imported from `package:flutter_soloud/flutter_soloud.dart`.115116### Live117118- `void setVisualizationEnabled(bool enabled, {int windowSize = 256, VisualizationKind kind = VisualizationKind.waveAndFft, int channel = VisualizationChannel.merged})` — turns the analyzer on/off. Must be called after `init()`; throws `SoLoudNotInitializedException` otherwise, `SoLoudCppException` on bad params.119- `Stream<AudioVisualizationData> get audioVisualizationEvents` — broadcast stream; emits one packet per mixer buffer while enabled.120- `AudioVisualizationData` — `channelCount`, `wave` (`List<Float32List>`, one per channel, length `windowSize`, values `[-1.0, 1.0]`), `fft` (`List<Float32List>`, one per channel, length `windowSize / 2`, magnitudes `[0.0, 1.0]`). Either list is **empty** if its kind is disabled. `waveData` / `fftData` are nullable convenience getters for `wave.first` / `fft.first`.121- `VisualizationKind` — real enum: `wave`, `fft`, `waveAndFft`.122- `VisualizationChannel` — **not an enum**, an `abstract final class` of int constants: `merged = -1` (downmix to mono), `all = -2` (per-channel lists), or pass any 0-based channel index (`0` = left). There is no `VisualizationChannel.values`; don't switch over it.123- `void setFftSmoothing(double smooth)` — exponential decay on falling FFT bins, `0.0`–`1.0`. Rise is never smoothed.124- `double getApproximateVolume(int channel)` — instantaneous overall output volume per *output* channel (speaker). Poll it on a `Timer` for a VU meter. Returns 0 for invalid channels.125126### Offline127128- `Future<Float32List> readSamplesFromFile(String completeFileName, int numSamplesNeeded, {double startTime = 0, double endTime = -1, bool average = false})` — native platforms only.129- `Future<Float32List> readSamplesFromMem(Uint8List buffer, int numSamplesNeeded, {double startTime = 0, double endTime = -1, bool average = false})` — everywhere; `buffer` is the encoded file bytes (mp3/wav/flac/ogg, from assets, `File.readAsBytes()`, or a network fetch).130- Times are in **seconds**; `endTime: -1` means "to end of file". `average: true` makes each returned value the mean of its time bucket instead of a single point sample — smoother waveforms, negligible cost.131132### Divergences from audioplayers / just_audio / web Audio API assumptions133134- No `AudioPlayer`-style per-sound analyser. Live visualization taps the **whole mixed engine output** (all voices, post-filter), not an individual track.135- There is no `onPositionChanged`-style callback to hook FFT onto; you enable the analyzer once and consume a stream.136- `getApproximateVolume` is per **output channel**, not per voice — there is no built-in per-sound level meter.137- The offline readers don't require `init()` and don't need the sound loaded via `loadMem`/`loadAsset` first — they decode independently (asset bytes via `rootBundle.load` are fine).138139## Traps140141- **Silent stream.** `audioVisualizationEvents` emits nothing until `setVisualizationEnabled(true)` is called, and calling it before `init()` completes throws. If your painter stays black, check enablement and init order first.142- **`windowSize` must be a power of two in 128–8192.** Anything else fails with `SoLoudCppException`, not an assertion. FFT bin count is `windowSize / 2`, wave length is `windowSize` — don't assume they're equal.143- **Empty lists by kind.** With `kind: VisualizationKind.fft`, `data.wave` is empty and `data.waveData` is `null` — null-check the convenience getters.144- **Every event is a fresh copy.** The binding copies native memory into new `Float32List`s per packet (`Float32List.fromList`), so retaining an event is safe and cheap to reason about — but don't hold a queue of them unbounded. Note: older docs mention `wavePointer`/`fftPointer` getters on `AudioVisualizationData`; they do not exist in this version of the package.145- **Event rate is tied to `bufferSize`, not frames.** With the default `bufferSize: 2048` at 44.1 kHz you get ~21 packets/s; the example uses `init(bufferSize: 1024)` (~43/s) for smoother visuals. Don't do heavy layout per event; a `CustomPainter` repaint is fine.146- **`deinit()` disables visualization** (and `deinit()` is called when `init()` re-initializes). Re-enable after re-init.147- **`readSamplesFromFile` throws/is unavailable on Web** — gate on `kIsWeb` and use `readSamplesFromMem` with the file bytes. On Web, `readSamplesFromMem` runs synchronously (no real isolate) and can jank the UI for large files.148- **Returned length ≠ `numSamplesNeeded`.** The list is shorter if `endTime` overshoots the file duration. Size your painter from `data.length`, never from the requested count.149- **Asserts on time range:** `startTime >= 0` and `endTime > startTime` (unless `-1`). Passing a `Duration` won't compile — convert with `duration.inMilliseconds / 1000`.150- **Per-channel indexing is by mixer output channels.** With `channel: VisualizationChannel.all`, `data.wave[0]` is left, `[1]` is right — the number of entries equals the engine's `Channels` setting at `init()`, not the file's channel count.151152## More depth153154- [references/live_painter_recipe.md](references/live_painter_recipe.md) — full per-channel wave + FFT `CustomPainter` widget, adapted from the plugin example.155- [references/offline_waveform.md](references/offline_waveform.md) — full `readSamplesFromMem` flow with file picking, averaging, and the waveform painter.156- Live demo: `example/lib/audio_data/audio_data.dart` (+ `data_widget.dart`). Offline demo: `example/lib/wave_data/wave_data.dart`.157158## Keeping this skill current159160This skill ships inside the flutter_soloud package, so upgrading flutter_soloud can carry a newer revision of it than the copy installed in the project. To check, run:161162```sh163dart run flutter_soloud:skills --check164```165166It reports the installed and bundled skill versions and exits non-zero when an update is available. Offer to update with `dart run flutter_soloud:skills` (which touches only the skills, never pubspec or build files).