simple_audio_kit 1.2.0
simple_audio_kit: ^1.2.0 copied to clipboard
A concise audio playback library for Flutter with playlists, caching, background controls, audio focus, and typed errors.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:simple_audio_kit/simple_audio_kit.dart';
// Sample demonstration tracks demonstrating generic audio types & metadata
final List<AudioItem> _musicPlaylist = [
AudioItem.network(
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
id: 'music_1',
title: 'SoundHelix Song 1',
artist: 'SoundHelix',
album: 'Electronic Fusion',
genre: 'Synthwave',
cache: true,
image:
'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?auto=format&fit=crop&w=500&q=60',
),
AudioItem.network(
'https://cdn.pixabay.com/audio/2022/03/15/audio_7ce11e3b5e.mp3',
id: 'music_2',
title: 'Cinematic Atmosphere',
artist: 'Pixabay Audio',
album: 'Ambient Dreams',
genre: 'Cinematic',
image:
'https://images.unsplash.com/photo-1536440136628-849c177e76a1?auto=format&fit=crop&w=500&q=60',
),
AudioItem.network(
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
id: 'music_3',
title: 'SoundHelix Song 3',
artist: 'SoundHelix',
album: 'Chillout Sessions',
genre: 'Chillout',
image:
'https://images.unsplash.com/photo-1514525253161-7a46d19cd819?auto=format&fit=crop&w=500&q=60',
),
];
final List<AudioItem> _podcastPlaylist = [
AudioItem.network(
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3',
id: 'podcast_1',
title: 'Ep. 42: Future of Flutter & Audio',
artist: 'TechTalks Podcast',
album: 'Developer Daily',
genre: 'Podcast',
image:
'https://images.unsplash.com/photo-1590602847861-f357a9332bbc?auto=format&fit=crop&w=500&q=60',
extras: const {'episodeNumber': 42, 'topic': 'Mobile Audio Engines'},
),
AudioItem.network(
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3',
id: 'podcast_2',
title: 'Ep. 43: Building Cross-Platform Apps',
artist: 'TechTalks Podcast',
album: 'Developer Daily',
genre: 'Podcast',
image:
'https://images.unsplash.com/photo-1478737270239-2f02b77fc618?auto=format&fit=crop&w=500&q=60',
extras: const {'episodeNumber': 43, 'topic': 'System Integration'},
),
];
final List<AudioItem> _brokenPlaylist = [
AudioItem.network(
'https://this.is.a.broken.domain.example.com/missing.mp3',
id: 'error_1',
title: 'Unreachable Stream (Error Test)',
artist: 'Error Recovery Test',
genre: 'Diagnostic',
image:
'https://images.unsplash.com/photo-1520697830682-8f15eee029ae?auto=format&fit=crop&w=500&q=60',
),
];
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Initialize global audio service
await Audio.init();
runApp(const SimpleAudioExampleApp());
}
class SimpleAudioExampleApp extends StatelessWidget {
const SimpleAudioExampleApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Simple Audio Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.teal,
brightness: Brightness.dark,
),
useMaterial3: true,
),
home: const ExampleHome(),
debugShowCheckedModeBanner: false,
);
}
}
class ExampleHome extends StatefulWidget {
const ExampleHome({super.key});
@override
State<ExampleHome> createState() => _ExampleHomeState();
}
class _ExampleHomeState extends State<ExampleHome>
with SingleTickerProviderStateMixin {
StreamSubscription? _errorSub;
// Standalone secondary controller for concurrent sound effects / multi-player demo
late final SimpleAudioController _sfxController;
int _selectedCategoryIndex = 0; // 0: Music, 1: Podcast, 2: Error Test
@override
void initState() {
super.initState();
_sfxController = SimpleAudioController(
sessionConfiguration: const AudioSessionConfiguration.music(),
);
_errorSub = Audio.errorStream.listen((e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const Icon(Icons.error_outline, color: Colors.white),
const SizedBox(width: 8),
Expanded(child: Text(e.message)),
],
),
backgroundColor: Colors.redAccent.shade700,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
);
}
});
// Load initial music playlist and pause
Audio.playList(_musicPlaylist).then((_) => Audio.pause());
}
@override
void dispose() {
_errorSub?.cancel();
_sfxController.dispose();
super.dispose();
}
List<AudioItem> get _activePlaylist {
switch (_selectedCategoryIndex) {
case 0:
return _musicPlaylist;
case 1:
return _podcastPlaylist;
case 2:
return _brokenPlaylist;
default:
return _musicPlaylist;
}
}
Future<void> _switchCategory(int index) async {
setState(() {
_selectedCategoryIndex = index;
});
await Audio.playList(_activePlaylist);
}
Future<void> _updateAudioSession(
AudioSessionConfiguration config,
String label,
) async {
await Audio.init(sessionConfiguration: config);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Audio session updated to $label'),
duration: const Duration(seconds: 2),
behavior: SnackBarBehavior.floating,
),
);
}
}
Future<void> _playConcurrentSfx() async {
await _sfxController.init();
await _sfxController.play(
'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-16.mp3',
title: 'Concurrent Secondary Sound FX',
);
}
String _formatDuration(Duration d) {
final m = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final s = d.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$m:$s';
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text(
'Simple Audio Demo',
style: TextStyle(fontWeight: FontWeight.bold),
),
centerTitle: false,
actions: [
PopupMenuButton<AudioSessionConfiguration>(
icon: const Icon(Icons.settings_suggest_outlined),
tooltip: 'Audio Session Category',
onSelected: (config) {
final label = config == const AudioSessionConfiguration.music()
? 'Music'
: 'Speech / Podcast';
_updateAudioSession(config, label);
},
itemBuilder: (context) => [
const PopupMenuItem(
value: AudioSessionConfiguration.music(),
child: Row(
children: [
Icon(Icons.music_note, color: Colors.tealAccent),
SizedBox(width: 8),
Text('Music Mode'),
],
),
),
const PopupMenuItem(
value: AudioSessionConfiguration.speech(),
child: Row(
children: [
Icon(Icons.podcasts, color: Colors.tealAccent),
SizedBox(width: 8),
Text('Speech Mode (Podcasts)'),
],
),
),
],
),
],
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. Now Playing Hero Card
_NowPlayingCard(formatDuration: _formatDuration),
const SizedBox(height: 16),
// 2. Playback Speed & Volume Bar
_SecondaryControlsCard(),
const SizedBox(height: 16),
// 3. Multi-Player Concurrent Sound Effect Demo
Card(
color: theme.colorScheme.surfaceContainerHigh,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12.0,
),
child: Row(
children: [
const Icon(
Icons.layers_outlined,
color: Colors.tealAccent,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Multi-Player Demo',
style: theme.textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'Play secondary SFX with SimpleAudioController',
style: theme.textTheme.bodySmall?.copyWith(
color: Colors.white70,
),
),
],
),
),
ElevatedButton.icon(
onPressed: _playConcurrentSfx,
icon: const Icon(Icons.volume_up, size: 18),
label: const Text('Play SFX'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.teal,
foregroundColor: Colors.white,
),
),
],
),
),
),
const SizedBox(height: 20),
// 4. Playlist Category Filter Chips
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Generic Audio Library',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
'${_activePlaylist.length} items',
style: theme.textTheme.labelMedium?.copyWith(
color: Colors.white54,
),
),
],
),
const SizedBox(height: 8),
SegmentedButton<int>(
segments: const [
ButtonSegment(
value: 0,
label: Text('Music'),
icon: Icon(Icons.library_music_outlined),
),
ButtonSegment(
value: 1,
label: Text('Podcasts'),
icon: Icon(Icons.podcasts_outlined),
),
ButtonSegment(
value: 2,
label: Text('Errors'),
icon: Icon(Icons.error_outline),
),
],
selected: {_selectedCategoryIndex},
onSelectionChanged: (selected) {
_switchCategory(selected.first);
},
),
const SizedBox(height: 12),
// 5. Active Playlist Items
StreamBuilder<AudioItem?>(
stream: Audio.currentItemStream,
initialData: Audio.currentItemNow,
builder: (context, snapshot) {
final current = snapshot.data;
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _activePlaylist.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, i) {
final item = _activePlaylist[i];
final isCurrent = current?.url == item.url;
return ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
tileColor: isCurrent
? Colors.teal.withValues(alpha: 0.15)
: null,
leading: _Artwork(url: item.image, size: 48),
title: Text(
item.title ?? 'Untitled Track',
style: TextStyle(
fontWeight: isCurrent
? FontWeight.bold
: FontWeight.normal,
color: isCurrent ? Colors.tealAccent : null,
),
),
subtitle: Text(
[
if (item.artist != null) item.artist,
if (item.album != null) item.album,
if (item.genre != null) '[${item.genre}]',
].join(' • '),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall,
),
trailing: isCurrent
? const Icon(
Icons.graphic_eq,
color: Colors.tealAccent,
)
: const Icon(
Icons.play_circle_outline,
color: Colors.white54,
),
onTap: () {
// Play from this item onwards
Audio.playList(_activePlaylist.sublist(i));
},
);
},
);
},
),
],
),
),
),
);
}
}
class _NowPlayingCard extends StatelessWidget {
const _NowPlayingCard({required this.formatDuration});
final String Function(Duration d) formatDuration;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Card(
elevation: 6,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
children: [
// Track Info & Artwork
StreamBuilder<AudioItem?>(
stream: Audio.currentItemStream,
initialData: Audio.currentItemNow,
builder: (context, snapshot) {
final current = snapshot.data;
final extras = current?.extras;
return Column(
children: [
_Artwork(url: current?.image, size: 120),
const SizedBox(height: 16),
Text(
current?.title ?? 'No audio loaded',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
current?.artist ?? '—',
style: theme.textTheme.bodyMedium?.copyWith(
color: Colors.tealAccent,
),
textAlign: TextAlign.center,
),
if (current?.album != null || current?.genre != null) ...[
const SizedBox(height: 6),
Wrap(
alignment: WrapAlignment.center,
spacing: 8,
children: [
if (current?.album != null)
Chip(
label: Text(
current!.album!,
style: const TextStyle(fontSize: 11),
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
if (current?.genre != null)
Chip(
label: Text(
current!.genre!,
style: const TextStyle(fontSize: 11),
),
backgroundColor: Colors.teal.withValues(
alpha: 0.3,
),
padding: EdgeInsets.zero,
visualDensity: VisualDensity.compact,
),
],
),
],
if (extras != null && extras.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'Extras: ${extras.entries.map((e) => '${e.key}: ${e.value}').join(', ')}',
style: theme.textTheme.labelSmall?.copyWith(
color: Colors.white54,
),
),
],
],
);
},
),
const SizedBox(height: 16),
// Progress Slider
StreamBuilder<Duration?>(
stream: Audio.durationStream,
builder: (context, durationSnap) {
return StreamBuilder<Duration>(
stream: Audio.positionStream,
builder: (context, positionSnap) {
final duration = durationSnap.data ?? Duration.zero;
final position = positionSnap.data ?? Duration.zero;
double progress = 0.0;
if (duration.inMilliseconds > 0) {
progress =
position.inMilliseconds / duration.inMilliseconds;
}
return Column(
children: [
SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: 4,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 6,
),
),
child: Slider(
value: progress.clamp(0.0, 1.0),
activeColor: Colors.tealAccent,
inactiveColor: Colors.white24,
onChanged: (value) {
if (duration.inMilliseconds > 0) {
Audio.seek(
Duration(
milliseconds:
(value * duration.inMilliseconds)
.toInt(),
),
);
}
},
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
formatDuration(position),
style: theme.textTheme.labelSmall,
),
Text(
formatDuration(duration),
style: theme.textTheme.labelSmall,
),
],
),
],
);
},
);
},
),
const SizedBox(height: 16),
// Playback Transport Controls
StreamBuilder<AudioPlaybackState>(
stream: Audio.playbackStateStream,
initialData: Audio.playbackStateNow,
builder: (context, snapshot) {
final isPlaying = snapshot.data == AudioPlaybackState.playing;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Repeat Mode Toggle
StreamBuilder<AudioRepeatMode>(
stream: Audio.repeatModeStream,
initialData: AudioRepeatMode.off,
builder: (context, snap) {
final mode = snap.data ?? AudioRepeatMode.off;
final icon = mode == AudioRepeatMode.one
? Icons.repeat_one
: Icons.repeat;
final color = mode == AudioRepeatMode.off
? Colors.grey
: Colors.tealAccent;
return IconButton(
icon: Icon(icon, color: color),
tooltip: 'Repeat: ${mode.name}',
onPressed: () {
final next = mode == AudioRepeatMode.off
? AudioRepeatMode.all
: mode == AudioRepeatMode.all
? AudioRepeatMode.one
: AudioRepeatMode.off;
Audio.setRepeatMode(next);
},
);
},
),
IconButton(
icon: const Icon(Icons.skip_previous, size: 36),
onPressed: Audio.previous,
),
FloatingActionButton.large(
elevation: 2,
backgroundColor: Colors.tealAccent,
foregroundColor: Colors.black,
onPressed: isPlaying ? Audio.pause : Audio.resume,
child: Icon(
isPlaying ? Icons.pause : Icons.play_arrow,
size: 40,
),
),
IconButton(
icon: const Icon(Icons.skip_next, size: 36),
onPressed: Audio.next,
),
// Shuffle Toggle
StreamBuilder<bool>(
stream: Audio.shuffleStream,
initialData: false,
builder: (context, snap) {
final isShuffled = snap.data ?? false;
return IconButton(
icon: Icon(
Icons.shuffle,
color: isShuffled ? Colors.tealAccent : Colors.grey,
),
tooltip: 'Shuffle',
onPressed: () => Audio.setShuffle(!isShuffled),
);
},
),
],
);
},
),
],
),
),
);
}
}
class _SecondaryControlsCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
child: Row(
children: [
// Volume Control
const Icon(Icons.volume_down, size: 20, color: Colors.white70),
Expanded(
child: StreamBuilder<double>(
stream: Audio.volumeStream,
initialData: 1.0,
builder: (context, snapshot) {
final volume = snapshot.data ?? 1.0;
return Slider(
value: volume,
activeColor: Colors.tealAccent,
onChanged: Audio.setVolume,
);
},
),
),
const Icon(Icons.volume_up, size: 20, color: Colors.white70),
const SizedBox(width: 16),
const SizedBox(height: 24, child: VerticalDivider(width: 1)),
const SizedBox(width: 8),
// Speed Multiplier
StreamBuilder<double>(
stream: Audio.speedStream,
initialData: 1.0,
builder: (context, snapshot) {
final speed = snapshot.data ?? 1.0;
return ActionChip(
avatar: const Icon(Icons.speed, size: 16),
label: Text('${speed}x'),
onPressed: () {
final nextSpeed = speed == 1.0
? 1.25
: speed == 1.25
? 1.5
: speed == 1.5
? 2.0
: speed == 2.0
? 0.5
: 1.0;
Audio.setSpeed(nextSpeed);
},
);
},
),
],
),
),
);
}
}
class _Artwork extends StatelessWidget {
const _Artwork({required this.url, this.size = 48});
final String? url;
final double size;
@override
Widget build(BuildContext context) {
final placeholder = Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.white12,
),
child: Icon(Icons.music_note, size: size / 2, color: Colors.white38),
);
final source = url?.trim();
if (source == null || source.isEmpty) return placeholder;
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
source,
width: size,
height: size,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => placeholder,
),
);
}
}