Creating an engaging audio visualizer enhances the interactivity of any app. This guide will walk you through building a retro arcade-style rainbow visualizer using Flutter and the audio_visualizer package.
Preview
Below is a sneak peek of the final output:
Step 1: Setting Up the Visualizer
We start by creating a RainbowBlockVisualizer
widget. This widget will dynamically render audio blocks based on FFT (Fast Fourier Transform) data. Here's the implementation:
RainbowBlockVisualizer Widget
import 'dart:math';
import 'package:flutter/material.dart';
class RainbowBlockVisualizer extends StatelessWidget {
final List<int> data;
final int maxSample;
final double blockHeight;
final double blockSpacing;
const RainbowBlockVisualizer({
super.key,
this.data = const [],
this.maxSample = 32,
this.blockHeight = 8,
this.blockSpacing = 1,
});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: AudioBlockPainter(
data: data,
blockHeight: blockHeight,
blockSpacing: blockSpacing,
maxSample: maxSample,
),
size: Size.infinite,
);
}
}
Custom Painter for Audio Blocks
class AudioBlockPainter extends CustomPainter {
final List<int> data;
final double blockHeight;
final double blockSpacing;
final int maxSample;
AudioBlockPainter({
required this.data,
required this.blockHeight,
required this.blockSpacing,
required this.maxSample,
});
int get dataLength => min(maxSample, data.length);
int getNumBlocks(int audioValue) {
return (audioValue / 255 * 32).round().clamp(1, 32);
}
Color getBlockColor(int barIndex, int blockIndex, int maxBlocks) {
final hue = (barIndex / dataLength) * 360;
const lightnessRange = 0.1;
final lightness = 0.6 +
(blockIndex / maxBlocks * lightnessRange).clamp(0.0, lightnessRange);
return HSLColor.fromAHSL(1, hue, 0.8, lightness).toColor();
}
@override
void paint(Canvas canvas, Size size) {
if (data.isEmpty) return;
final barWidth = size.width / dataLength;
for (int i = 0; i < dataLength; i++) {
final numBlocks = getNumBlocks(data[i]);
final barX = i * barWidth;
for (int j = 0; j < numBlocks; j++) {
final paint = Paint()..color = getBlockColor(i, j, numBlocks);
final blockY = size.height - ((j + 1) * (blockHeight + blockSpacing));
final rect = RRect.fromRectAndRadius(
Rect.fromLTWH(
barX + blockSpacing / 2,
blockY,
barWidth - blockSpacing,
blockHeight,
),
const Radius.circular(2),
);
canvas.drawRRect(rect, paint);
final highlightPaint = Paint()
..color = Colors.white.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawLine(
Offset(rect.left, rect.top),
Offset(rect.right, rect.top),
highlightPaint,
);
canvas.drawLine(
Offset(rect.left, rect.top),
Offset(rect.left, rect.bottom),
highlightPaint,
);
final shadowPaint = Paint()
..color = Colors.black.withOpacity(0.2)
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawLine(
Offset(rect.right, rect.top),
Offset(rect.right, rect.bottom),
shadowPaint,
);
canvas.drawLine(
Offset(rect.left, rect.bottom),
Offset(rect.right, rect.bottom),
shadowPaint,
);
}
}
}
@override
bool shouldRepaint(AudioBlockPainter oldDelegate) {
return data != oldDelegate.data ||
blockHeight != oldDelegate.blockHeight ||
blockSpacing != oldDelegate.blockSpacing;
}
}
Step 2: Creating the Main Application
Main Function
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'rainbow_visualizer.dart';
void main() {
runApp(MaterialApp(
themeMode: ThemeMode.dark,
darkTheme: ThemeData.dark(useMaterial3: true),
home: const MyApp(),
));
}
MyApp Widget
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
final audioPlayer = VisualizerPlayer();
final sources = [
"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3",
"https://files.testfile.org/AUDIO/C/M4A/sample1.m4a",
];
var sourceIndex = 0;
@override
void initState() {
super.initState();
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
audioPlayer.initialize();
audioPlayer.setDataSource(sources[sourceIndex]);
audioPlayer.addListener(onUpdate);
}
void onUpdate() {
if (audioPlayer.value.status == PlayerStatus.ready) {
audioPlayer.play(looping: true);
}
}
@override
void dispose() {
audioPlayer.removeListener(onUpdate);
audioPlayer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Center(
child: Text('Audio Visualizer'),
),
),
body: Column(
children: [
const SizedBox(height: 32),
Padding(
padding: const EdgeInsets.all(8.0),
child: ListenableBuilder(
listenable: audioPlayer,
builder: (context, child) {
final duration = audioPlayer.value.duration;
final position = audioPlayer.value.position;
final durationText = formatDuration(duration);
final positionText = formatDuration(position);
return Text(
"$positionText / $durationText",
style: Theme.of(context).textTheme.headlineLarge,
);
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: ListenableBuilder(
listenable: audioPlayer,
builder: (context, child) {
return Text(
audioPlayer.value.status == PlayerStatus.playing
? "Now Playing"
: "",
style: Theme.of(context).textTheme.bodyLarge,
);
},
),
),
Expanded(
child: ListenableBuilder(
listenable: audioPlayer,
builder: (context, child) {
final data = getMagnitudes(audioPlayer.value.fft);
return AudioSpectrum(
fftMagnitudes: data,
bandType: BandType.tenBand,
builder: (context, value, child) {
return RainbowBlockVisualizer(
data: value.levels,
maxSample: 32,
blockHeight: 14,
);
},
);
},
),
),
ListenableBuilder(
listenable: audioPlayer,
builder: (context, child) {
final duration = audioPlayer.value.duration.inMilliseconds;
final position = audioPlayer.value.position.inMilliseconds;
return LinearProgressIndicator(
value: position / max(1, duration),
minHeight: 8,
);
},
),
],
),
bottomNavigationBar: BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () {
audioPlayer.play(looping: true);
},
),
IconButton(
icon: const Icon(Icons.pause),
onPressed: () {
audioPlayer.pause();
},
),
IconButton(
icon: const Icon(Icons.stop),
onPressed: () {
audioPlayer.stop();
},
),
IconButton(
icon: const Icon(Icons.loop),
onPressed: () {
sourceIndex = (sourceIndex + 1) % sources.length;
audioPlayer.stop();
audioPlayer.setDataSource(sources[sourceIndex]);
},
),
],
),
),
);
}
}
String formatDuration(Duration duration) {
final minutes = duration.inMinutes;
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
return "$minutes:$seconds";
}
Final Notes
This app integrates the audio_visualizer
package to transform FFT data into a visually appealing audio spectrum. Experiment with different colors, block sizes, and spacing for unique designs!
Top comments (0)