simple_audio_kit 1.2.0 copy "simple_audio_kit: ^1.2.0" to clipboard
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.

example/lib/main.dart

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,
      ),
    );
  }
}
2
likes
160
points
91
downloads

Documentation

API reference

Publisher

verified publisherwebcode.codes

Weekly Downloads

A concise audio playback library for Flutter with playlists, caching, background controls, audio focus, and typed errors.

Repository (GitHub)
View/report issues

Topics

#audio #music #player #playback

License

MIT (license)

Dependencies

audio_service, audio_session, equatable, flutter, just_audio, rxdart

More

Packages that depend on simple_audio_kit