flutter_recorder audio visualization
flutter_recorder features a high-performance, SIMD-accelerated (via PFFFT) audio analysis engine with Blackman windowing and exponential frequency smoothing. It delivers time-domain waveform samples and frequency-domain FFT magnitude bins in real time.
Minimal example
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_recorder/flutter_recorder.dart';
Future<void> setupVisualization() async {
final recorder = Recorder.instance;
// 1. MUST initialize with PCMFormat.f32le
await recorder.init(
format: PCMFormat.f32le,
sampleRate: 44100,
channels: RecorderChannels.mono,
);
recorder.start();
// 2. Configure visualization engine
recorder.setVisualizationEnabled(
true,
windowSize: 512, // Power of 2 (128..8192)
kind: VisualizationKind.waveAndFft, // wave, fft, or waveAndFft
channel: VisualizationChannel.merged,// mono downmix or channel index
);
// 3. Optional: Configure FFT smoothing for calmer animations
recorder.setFftSmoothing(0.6); // 0.0 = instant, 1.0 = frozen
// 4. Listen to visualization events
recorder.audioVisualizationEvents.listen((AudioVisualizationData data) {
// Waveform samples: values in range [-1.0, 1.0]
final Float32List? wave = data.waveData;
// FFT frequency bins: values in range [0.0, 1.0] (length: windowSize / 2)
final Float32List? fft = data.fftData;
// Volume level in dB: [-100, 0]
final double volumeDb = recorder.getVolumeDb();
});
}
The API Shape
All visualization APIs live on Recorder.instance:
void setVisualizationEnabled(bool enabled, {int windowSize = 256, VisualizationKind kind = VisualizationKind.waveAndFft, int channel = VisualizationChannel.merged}): Starts or stops visual analysis.windowSize: Power of 2 between 128 and 8192 (128, 256, 512, 1024, 2048, 4096, 8192).kind:VisualizationKind.wave,VisualizationKind.fft, orVisualizationKind.waveAndFft.channel:VisualizationChannel.merged(-1 mono downmix),VisualizationChannel.all(-2), or 0-indexed channel.
bool getVisualizationEnabled(): Checks whether visual analysis is active.Stream<AudioVisualizationData> get audioVisualizationEvents: Emits analysis packets containing waveform and FFT data.void setVisualizationCallback(void Function(AudioVisualizationData data)? callback): Direct callback alternative toaudioVisualizationEvents.void setFftSmoothing(double smooth): Smooths decreasing FFT values (0.0 to 1.0).0.0means immediate response;0.6provides natural bar decay.double getVolumeDb(): Computes instantaneous RMS volume in dB[-100, 0].
Data Object: AudioVisualizationData
int channelCount: Number of audio channels in this packet.List<Float32List> wave: Waveform buffers per channel ([-1.0, 1.0]).List<Float32List> fft: FFT magnitude buffers per channel ([0.0, 1.0], lengthwindowSize / 2).Float32List? get waveData: First/merged channel waveform.Float32List? get fftData: First/merged channel FFT bins.
UI Recipe: Frequency Spectrum Painter (CustomPainter)
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_recorder/flutter_recorder.dart';
class SpectrumVisualizer extends StatelessWidget {
const SpectrumVisualizer({super.key, required this.data});
final AudioVisualizationData? data;
@override
Widget build(BuildContext context) {
return CustomPaint(
size: const Size(double.infinity, 120),
painter: FftBarsPainter(data?.fftData),
);
}
}
class FftBarsPainter extends CustomPainter {
FftBarsPainter(this.fft);
final Float32List? fft;
@override
void paint(Canvas canvas, Size size) {
if (fft == null || fft!.isEmpty) return;
final paint = Paint()
..color = Colors.cyanAccent
..style = PaintingStyle.fill;
const barCount = 32; // Aggregate bins into 32 visible bars
final binStep = fft!.length ~/ barCount;
final barWidth = size.width / barCount;
for (int i = 0; i < barCount; i++) {
double maxMagnitude = 0.0;
for (int b = 0; b < binStep; b++) {
final binIdx = i * binStep + b;
if (binIdx < fft!.length && fft![binIdx] > maxMagnitude) {
maxMagnitude = fft![binIdx];
}
}
final barHeight = (maxMagnitude * size.height).clamp(2.0, size.height);
final left = i * barWidth;
final top = size.height - barHeight;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(left + 1, top, barWidth - 2, barHeight),
const Radius.circular(2),
),
paint,
);
}
}
@override
bool shouldRepaint(covariant FftBarsPainter oldDelegate) => true;
}
UI Recipe: Oscilloscope Waveform Painter
import 'dart:typed_data';
import 'package:flutter/material.dart';
class WaveformPainter extends CustomPainter {
WaveformPainter(this.wave);
final Float32List? wave;
@override
void paint(Canvas canvas, Size size) {
if (wave == null || wave!.isEmpty) return;
final paint = Paint()
..color = Colors.greenAccent
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
final path = Path();
final step = size.width / wave!.length;
final centerY = size.height / 2;
for (int i = 0; i < wave!.length; i++) {
final x = i * step;
final y = centerY + (wave![i] * centerY);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant WaveformPainter oldDelegate) => true;
}
UI Recipe: VU Volume Meter
import 'package:flutter/material.dart';
class VuMeterWidget extends StatelessWidget {
const VuMeterWidget({super.key, required this.volumeDb});
final double volumeDb; // e.g. -100 to 0 dB
@override
Widget build(BuildContext context) {
// Normalize -60 dB .. 0 dB into 0.0 .. 1.0
final normalized = ((volumeDb + 60) / 60).clamp(0.0, 1.0);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(
value: normalized,
backgroundColor: Colors.grey.shade800,
color: normalized > 0.85
? Colors.red
: (normalized > 0.6 ? Colors.amber : Colors.green),
minHeight: 12,
),
Text('${volumeDb.toStringAsFixed(1)} dB'),
],
);
}
}
Traps & Gotchas
- Requires
PCMFormat.f32le: Setting visualization with any format other thanPCMFormat.f32leis ignored with a warning. - Window sizes must be powers of 2:
windowSizemust be in[128, 256, 512, 1024, 2048, 4096, 8192]. 256 or 512 is recommended for 60fps UI visualizers. audioVisualizationEventsfrequency: Events are emitted periodically as new FFT windows fill. UseRepaintBoundaryaround CustomPainters to avoid repainting surrounding UI trees.- Decibel range:
getVolumeDb()returns-100.0when capture is inactive or silent.
Keeping this skill current
This skill ships inside the flutter_recorder package. To check for updates, run:
dart run flutter_recorder:skills --check