miniaudio_player 1.0.1 copy "miniaudio_player: ^1.0.1" to clipboard
miniaudio_player: ^1.0.1 copied to clipboard

Ultra-low-resource cross-platform audio player using miniaudio. Equalizer, pitch and rate are supported.

example/lib/main.dart

import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:miniaudio_player/miniaudio_player.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  MiniaudioPlayer.config(
    defaultBufferSize: 0,
    logLevel: MiniaudioLogLevel.verbose,
    defaultAudioDevice: AudioDevice.auto,
  );
  runApp(const MiniaudioPlayerExampleApp());
}

class AudioTrack {
  final String id;
  final String title;
  final String? assetPath;
  final String? filePath;
  final bool isGenerated;

  const AudioTrack({
    required this.id,
    required this.title,
    this.assetPath,
    this.filePath,
    this.isGenerated = false,
  });

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is AudioTrack && runtimeType == other.runtimeType && id == other.id;

  @override
  int get hashCode => id.hashCode;
}

class MiniaudioPlayerExampleApp extends StatelessWidget {
  const MiniaudioPlayerExampleApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Miniaudio Player Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(
          seedColor: Colors.deepPurple,
          brightness: Brightness.dark,
        ),
        useMaterial3: true,
      ),
      home: const PlayerScreen(),
    );
  }
}

class PlayerScreen extends StatefulWidget {
  const PlayerScreen({super.key});

  @override
  State<PlayerScreen> createState() => _PlayerScreenState();
}

class _PlayerScreenState extends State<PlayerScreen> {
  final MiniaudioPlayer _player = MiniaudioPlayer();
  List<AudioTrack> _availableTracks = [];
  AudioTrack? _selectedTrack;
  String? _samplePath;
  bool _isReady = false;
  bool _isLoadingTrack = false;

  Equalizer _currentEqualizer = Equalizer.flat;
  double _volume = 1.0;
  double _rate = 1.0;
  double _pitch = 1.0;
  double? _dragPositionMs;
  List<AudioDevice> _audioDevices = [AudioDevice.auto];
  StreamSubscription<AudioDevice>? _deviceSub;

  @override
  void initState() {
    super.initState();
    _prepareAudioTracks();
    _refreshAudioDevices();
    _deviceSub = _player.stream.audioDevice.listen((_) {
      _refreshAudioDevices();
    });
  }

  Future<void> _refreshAudioDevices() async {
    try {
      final devs = await MiniaudioPlayer.getAudioDevices();
      if (mounted) {
        setState(() {
          _audioDevices = devs;
        });
      }
    } catch (_) {}
  }

  bool _isAudioFile(String path) {
    final lower = path.toLowerCase();
    return miniAudioPlayerSupportedExtensions.any(
      (ext) => lower.endsWith('.$ext'),
    );
  }

  Future<void> _prepareAudioTracks() async {
    final List<AudioTrack> tracks = [];
    final Set<String> seenFilenames = {};

    void addTrack({
      required String id,
      required String title,
      String? assetPath,
      String? filePath,
      bool isGenerated = false,
    }) {
      final filename = (assetPath ?? filePath ?? id)
          .split(RegExp(r'[/\\]'))
          .last;
      if (seenFilenames.contains(filename)) return;
      seenFilenames.add(filename);
      tracks.add(
        AudioTrack(
          id: id,
          title: title,
          assetPath: assetPath,
          filePath: filePath,
          isGenerated: isGenerated,
        ),
      );
    }

    // 1. Scan Flutter AssetManifest strictly under assets/music/
    try {
      final manifest = await AssetManifest.loadFromAssetBundle(rootBundle);
      final assets = manifest.listAssets();
      for (final asset in assets) {
        if (asset.startsWith('assets/music/') && _isAudioFile(asset)) {
          final filename = asset.substring('assets/music/'.length);
          addTrack(id: asset, title: filename, assetPath: asset);
        }
      }
    } catch (e) {
      debugPrint('AssetManifest query note: $e');
    }

    // 2. Scan strictly assets/music on the local filesystem
    final searchDirs = [
      Directory('assets/music'),
      Directory('example/assets/music'),
    ];
    for (final dir in searchDirs) {
      if (dir.existsSync()) {
        for (final entity in dir.listSync()) {
          if (entity is File && _isAudioFile(entity.path)) {
            final filename = entity.path.split(RegExp(r'[/\\]')).last;
            addTrack(
              id: entity.path,
              title: filename,
              filePath: entity.path,
              assetPath: 'assets/music/$filename',
            );
          }
        }
      }
    }

    // Fallback: If no assets found, use the autogenerated sine wave as fallback
    if (tracks.isEmpty) {
      tracks.add(
        const AudioTrack(
          id: 'fallback_generated',
          title: 'Fallback Autogenerado (440Hz Sine)',
          isGenerated: true,
        ),
      );
    } else {
      // Also provide the option to switch to the generated test tone
      tracks.add(
        const AudioTrack(
          id: 'test_tone_generated',
          title: 'Tono autogenerado (440Hz Sine)',
          isGenerated: true,
        ),
      );
    }

    _availableTracks = tracks;
    final initialTrack = tracks.first;
    _selectedTrack = initialTrack;

    try {
      final path = await _resolveTrackPath(initialTrack);
      _samplePath = path;
      if (path.isNotEmpty) {
        await _player.action.open(path);
      }
    } catch (e, st) {
      debugPrint('Initial track open note: $e\n$st');
    } finally {
      if (mounted) {
        setState(() {
          _isReady = true;
        });
      }
    }
  }

  Future<Directory> _getTempDirectory() async {
    try {
      return await getTemporaryDirectory();
    } catch (_) {
      return Directory.systemTemp;
    }
  }

  Future<String> _resolveTrackPath(AudioTrack track) async {
    if (track.isGenerated) {
      return _generateFallbackAudio();
    }

    // 1. If explicit file path is provided and exists on disk
    if (track.filePath != null && File(track.filePath!).existsSync()) {
      return track.filePath!;
    }

    // 2. If asset path is provided, try extracting from Flutter rootBundle
    if (track.assetPath != null) {
      try {
        final byteData = await rootBundle.load(track.assetPath!);
        final tempDir = await _getTempDirectory();
        final fileName = track.assetPath!.split(RegExp(r'[/\\]')).last;
        final tempFile = File('${tempDir.path}/$fileName');
        if (!tempFile.existsSync() ||
            tempFile.lengthSync() != byteData.lengthInBytes) {
          await tempFile.writeAsBytes(
            byteData.buffer.asUint8List(
              byteData.offsetInBytes,
              byteData.lengthInBytes,
            ),
            flush: true,
          );
        }
        return tempFile.path;
      } catch (e) {
        debugPrint('Asset extraction error (${track.assetPath}): $e');
        // Fall through to filesystem checks
      }
    }

    // 3. Check filesystem locations
    final filename = (track.assetPath ?? track.id).split(RegExp(r'[/\\]')).last;
    final possiblePaths = [
      'assets/music/$filename',
      'example/assets/music/$filename',
    ];
    for (final path in possiblePaths) {
      final file = File(path);
      if (file.existsSync()) {
        return file.path;
      }
    }

    // 4. Ultimate fallback: generate audio
    return _generateFallbackAudio();
  }

  Future<String> _generateFallbackAudio() async {
    try {
      final tempDir = await _getTempDirectory();
      final file = File('${tempDir.path}/miniaudio_demo_440hz_generated.wav');

      if (!file.existsSync()) {
        const sampleRate = 44100;
        const durationSeconds = 10;
        const numSamples = sampleRate * durationSeconds;
        const dataSize = numSamples * 2;
        final byteData = ByteData(44 + dataSize);

        byteData.setUint8(0, 0x52); // R
        byteData.setUint8(1, 0x49); // I
        byteData.setUint8(2, 0x46); // F
        byteData.setUint8(3, 0x46); // F
        byteData.setUint32(4, 36 + dataSize, Endian.little);
        byteData.setUint8(8, 0x57); // W
        byteData.setUint8(9, 0x41); // A
        byteData.setUint8(10, 0x56); // V
        byteData.setUint8(11, 0x45); // E
        byteData.setUint8(12, 0x66); // f
        byteData.setUint8(13, 0x6D); // m
        byteData.setUint8(14, 0x74); // t
        byteData.setUint8(15, 0x20); // ' '
        byteData.setUint32(16, 16, Endian.little);
        byteData.setUint16(20, 1, Endian.little);
        byteData.setUint16(22, 1, Endian.little);
        byteData.setUint32(24, sampleRate, Endian.little);
        byteData.setUint32(28, sampleRate * 2, Endian.little);
        byteData.setUint16(32, 2, Endian.little);
        byteData.setUint16(34, 16, Endian.little);
        byteData.setUint8(36, 0x64); // d
        byteData.setUint8(37, 0x61); // a
        byteData.setUint8(38, 0x74); // t
        byteData.setUint8(39, 0x61); // a
        byteData.setUint32(40, dataSize, Endian.little);

        for (int i = 0; i < numSamples; i++) {
          final t = i / sampleRate;
          final sampleVal = (sin(2 * pi * 440.0 * t) * 32767 * 0.7).toInt();
          byteData.setInt16(
            44 + i * 2,
            sampleVal.clamp(-32768, 32767),
            Endian.little,
          );
        }

        await file.writeAsBytes(byteData.buffer.asUint8List(), flush: true);
      }

      return file.path;
    } catch (e) {
      debugPrint('Error generating fallback audio: $e');
      return '';
    }
  }

  Future<void> _selectTrack(AudioTrack track) async {
    setState(() {
      _isLoadingTrack = true;
      _selectedTrack = track;
    });

    try {
      final wasPlaying = _player.state.playing;
      await _player.action.stop();
      final path = await _resolveTrackPath(track);
      _samplePath = path;
      await _player.action.open(path);
      if (wasPlaying) {
        await _player.action.play();
      }
    } catch (e) {
      debugPrint('Error switching track: $e');
    } finally {
      if (mounted) {
        setState(() {
          _isLoadingTrack = false;
        });
      }
    }
  }

  @override
  void dispose() {
    _deviceSub?.cancel();
    _player.dispose();
    super.dispose();
  }

  String _formatDuration(Duration duration) {
    final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
    final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
    return '$minutes:$seconds';
  }

  @override
  Widget build(BuildContext context) {
    if (!_isReady) {
      return const Scaffold(body: Center(child: CircularProgressIndicator()));
    }

    return Scaffold(
      appBar: AppBar(title: const Text('miniaudio_player Demo')),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            _buildPlaybackCard(),
            const SizedBox(height: 16),
            _buildDspControlsCard(),
            const SizedBox(height: 16),
            _buildEqualizerCard(),
          ],
        ),
      ),
    );
  }

  Widget _buildPlaybackCard() {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Row(
              children: [
                Expanded(
                  child: InputDecorator(
                    decoration: const InputDecoration(
                      labelText: 'Pista de audio (Assets)',
                      border: OutlineInputBorder(),
                      contentPadding: EdgeInsets.symmetric(
                        horizontal: 12,
                        vertical: 4,
                      ),
                      prefixIcon: Icon(Icons.library_music),
                    ),
                    child: DropdownButtonHideUnderline(
                      child: DropdownButton<AudioTrack>(
                        value: _selectedTrack,
                        isExpanded: true,
                        items: _availableTracks.map((track) {
                          final isFallback = track.isGenerated;
                          return DropdownMenuItem<AudioTrack>(
                            value: track,
                            child: Row(
                              children: [
                                Icon(
                                  isFallback
                                      ? Icons.graphic_eq
                                      : Icons.audiotrack,
                                  size: 18,
                                  color: isFallback
                                      ? Colors.amberAccent
                                      : Colors.tealAccent,
                                ),
                                const SizedBox(width: 8),
                                Expanded(
                                  child: Text(
                                    track.title,
                                    overflow: TextOverflow.ellipsis,
                                  ),
                                ),
                              ],
                            ),
                          );
                        }).toList(),
                        onChanged: _isLoadingTrack
                            ? null
                            : (track) {
                                if (track != null && track != _selectedTrack) {
                                  _selectTrack(track);
                                }
                              },
                      ),
                    ),
                  ),
                ),
                if (_isLoadingTrack) ...[
                  const SizedBox(width: 12),
                  const SizedBox(
                    width: 20,
                    height: 20,
                    child: CircularProgressIndicator(strokeWidth: 2),
                  ),
                ],
              ],
            ),
            const SizedBox(height: 8),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Expanded(
                  child: Text(
                    'Archivo: ${_samplePath?.split(RegExp(r"[/\\]")).last ?? ''}',
                    style: Theme.of(context).textTheme.bodySmall,
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
                StreamBuilder<int>(
                  stream: _player.stream.audioBitrate,
                  initialData: _player.state.audioBitrate,
                  builder: (context, snapshot) {
                    final bitrate = snapshot.data ?? 0;
                    if (bitrate <= 0) return const SizedBox.shrink();
                    final kbps = (bitrate / 1000).round();
                    return Padding(
                      padding: const EdgeInsets.only(right: 8.0),
                      child: Chip(
                        labelPadding: const EdgeInsets.symmetric(horizontal: 4),
                        visualDensity: VisualDensity.compact,
                        avatar: const Icon(Icons.speed, size: 14),
                        label: Text(
                          '$kbps kbps',
                          style: const TextStyle(fontSize: 10),
                        ),
                      ),
                    );
                  },
                ),
                StreamBuilder<bool>(
                  stream: _player.stream.playing,
                  initialData: _player.state.playing,
                  builder: (context, snapshot) {
                    final isPlaying = snapshot.data ?? false;
                    return Chip(
                      labelPadding: const EdgeInsets.symmetric(horizontal: 4),
                      visualDensity: VisualDensity.compact,
                      label: Text(
                        isPlaying ? 'PLAYING' : 'STOPPED / PAUSED',
                        style: const TextStyle(fontSize: 10),
                      ),
                    );
                  },
                ),
              ],
            ),
            const SizedBox(height: 8),
            StreamBuilder<Duration>(
              stream: _player.stream.position,
              initialData: _player.state.position,
              builder: (context, posSnapshot) {
                final pos = posSnapshot.data ?? Duration.zero;
                final dur = _player.state.duration;
                final maxMs = dur.inMilliseconds > 0
                    ? dur.inMilliseconds.toDouble()
                    : 1.0;
                final currentMs = pos.inMilliseconds.toDouble().clamp(
                  0.0,
                  maxMs,
                );

                return Column(
                  children: [
                    Slider(
                      value: (_dragPositionMs ?? currentMs).clamp(0.0, maxMs),
                      max: maxMs,
                      onChangeStart: (val) {
                        setState(() => _dragPositionMs = val);
                      },
                      onChanged: (val) {
                        setState(() => _dragPositionMs = val);
                      },
                      onChangeEnd: (val) {
                        _player.action.seek(
                          Duration(milliseconds: val.toInt()),
                        );
                        setState(() => _dragPositionMs = null);
                      },
                    ),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        Text(
                          _formatDuration(
                            _dragPositionMs != null
                                ? Duration(
                                    milliseconds: _dragPositionMs!.toInt(),
                                  )
                                : pos,
                          ),
                        ),
                        Text(_formatDuration(dur)),
                      ],
                    ),
                  ],
                );
              },
            ),
            const SizedBox(height: 12),
            StreamBuilder<bool>(
              stream: _player.stream.playing,
              initialData: _player.state.playing,
              builder: (context, snapshot) {
                final isPlaying = snapshot.data ?? false;

                return Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    IconButton(
                      iconSize: 40,
                      icon: const Icon(Icons.stop),
                      onPressed: () => _player.action.stop(),
                    ),
                    const SizedBox(width: 16),
                    IconButton(
                      iconSize: 48,
                      icon: Icon(
                        isPlaying ? Icons.pause_circle : Icons.play_circle,
                      ),
                      onPressed: () {
                        if (isPlaying) {
                          _player.action.pause();
                        } else {
                          _player.action.play();
                        }
                      },
                    ),
                  ],
                );
              },
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildDspControlsCard() {
    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              'DSP Controls',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 12),
            Row(
              children: [
                const SizedBox(width: 70, child: Text('Volume')),
                Expanded(
                  child: Slider(
                    value: _volume,
                    min: 0.0,
                    max: 2.0,
                    divisions: 20,
                    label: '${(_volume * 100).toInt()}%',
                    onChanged: (v) {
                      setState(() => _volume = v);
                      _player.action.setVolume(v);
                    },
                  ),
                ),
                SizedBox(width: 45, child: Text('${(_volume * 100).toInt()}%')),
              ],
            ),
            Row(
              children: [
                const SizedBox(width: 70, child: Text('Rate')),
                Expanded(
                  child: Slider(
                    value: _rate,
                    min: 0.5,
                    max: 2.0,
                    divisions: 15,
                    label: '${_rate.toStringAsFixed(1)}x',
                    onChanged: (v) {
                      setState(() => _rate = v);
                      _player.action.setRate(v);
                    },
                  ),
                ),
                SizedBox(
                  width: 45,
                  child: Text('${_rate.toStringAsFixed(1)}x'),
                ),
              ],
            ),
            Row(
              children: [
                const SizedBox(width: 70, child: Text('Pitch')),
                Expanded(
                  child: Slider(
                    value: _pitch,
                    min: 0.5,
                    max: 2.0,
                    divisions: 15,
                    label: '${_pitch.toStringAsFixed(1)}x',
                    onChanged: (v) {
                      setState(() => _pitch = v);
                      _player.action.setPitch(v);
                    },
                  ),
                ),
                SizedBox(
                  width: 45,
                  child: Text('${_pitch.toStringAsFixed(1)}x'),
                ),
              ],
            ),
            const Divider(height: 24),
            Row(
              children: [
                const SizedBox(width: 70, child: Text('Buffer Size')),
                Expanded(
                  child: StreamBuilder<int>(
                    stream: _player.stream.bufferSize,
                    initialData: _player.state.bufferSize,
                    builder: (context, snapshot) {
                      final currentBuffer = snapshot.data ?? 0;
                      final standardValues = [0, 256, 512, 1024, 2048, 4096];
                      final value = standardValues.contains(currentBuffer)
                          ? currentBuffer
                          : 0;

                      return DropdownButton<int>(
                        value: value,
                        isExpanded: true,
                        items: const [
                          DropdownMenuItem(
                            value: 0,
                            child: Text('0 (Native Default)'),
                          ),
                          DropdownMenuItem(
                            value: 256,
                            child: Text('256 frames (~5.8ms @ 44.1kHz)'),
                          ),
                          DropdownMenuItem(
                            value: 512,
                            child: Text('512 frames (~11.6ms @ 44.1kHz)'),
                          ),
                          DropdownMenuItem(
                            value: 1024,
                            child: Text('1024 frames (~23.2ms @ 44.1kHz)'),
                          ),
                          DropdownMenuItem(
                            value: 2048,
                            child: Text('2048 frames (~46.4ms @ 44.1kHz)'),
                          ),
                          DropdownMenuItem(
                            value: 4096,
                            child: Text('4096 frames (~92.8ms @ 44.1kHz)'),
                          ),
                        ],
                        onChanged: (newBuf) {
                          if (newBuf != null) {
                            _player.action.setBufferSize(newBuf);
                          }
                        },
                      );
                    },
                  ),
                ),
              ],
            ),
            const Divider(height: 24),
            Row(
              children: [
                const SizedBox(width: 70, child: Text('Output')),
                Expanded(
                  child: StreamBuilder<AudioDevice>(
                    stream: _player.stream.audioDevice,
                    initialData: _player.state.audioDevice,
                    builder: (context, snapshot) {
                      final currentDev =
                          snapshot.data ?? _player.state.audioDevice;
                      final items = <AudioDevice>{
                        ..._audioDevices,
                        currentDev,
                      }.toList();

                      AudioDevice? selectedValue;
                      for (final item in items) {
                        if (item == currentDev) {
                          selectedValue = item;
                          break;
                        }
                      }
                      selectedValue ??= items.isNotEmpty ? items.first : null;

                      return DropdownButton<AudioDevice>(
                        value: selectedValue,
                        isExpanded: true,
                        items: items.map((dev) {
                          return DropdownMenuItem<AudioDevice>(
                            value: dev,
                            child: Text(
                              "${dev.isAuto ? '[AUTO] ' : ''}${dev.name}${dev.isDefault ? ' [Default]' : ''}",
                              overflow: TextOverflow.ellipsis,
                            ),
                          );
                        }).toList(),
                        onChanged: (newDev) async {
                          if (newDev != null) {
                            try {
                              final success = await _player.action.setDevice(
                                newDev,
                              );
                              if (!success && context.mounted) {
                                ScaffoldMessenger.of(context).showSnackBar(
                                  SnackBar(
                                    content: Text(
                                      'Device "${newDev.name}" is unavailable or disconnected. Falling back to default.',
                                    ),
                                    backgroundColor: Colors.orange,
                                    duration: const Duration(seconds: 3),
                                  ),
                                );
                              }
                            } catch (e) {
                              if (context.mounted) {
                                ScaffoldMessenger.of(context).showSnackBar(
                                  SnackBar(
                                    content: Text('Failed to set device: $e'),
                                    backgroundColor: Colors.red,
                                    duration: const Duration(seconds: 3),
                                  ),
                                );
                              }
                            }
                            _refreshAudioDevices();
                          }
                        },
                      );
                    },
                  ),
                ),
                IconButton(
                  icon: const Icon(Icons.refresh, size: 20),
                  tooltip: 'Refresh Audio Devices',
                  onPressed: _refreshAudioDevices,
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildEqualizerCard() {
    final bands = [
      (
        '60Hz',
        _currentEqualizer.hz60,
        (double v) => _currentEqualizer.copyWith(hz60: v),
      ),
      (
        '170Hz',
        _currentEqualizer.hz170,
        (double v) => _currentEqualizer.copyWith(hz170: v),
      ),
      (
        '310Hz',
        _currentEqualizer.hz310,
        (double v) => _currentEqualizer.copyWith(hz310: v),
      ),
      (
        '600Hz',
        _currentEqualizer.hz600,
        (double v) => _currentEqualizer.copyWith(hz600: v),
      ),
      (
        '1kHz',
        _currentEqualizer.hz1k,
        (double v) => _currentEqualizer.copyWith(hz1k: v),
      ),
      (
        '3kHz',
        _currentEqualizer.hz3k,
        (double v) => _currentEqualizer.copyWith(hz3k: v),
      ),
      (
        '6kHz',
        _currentEqualizer.hz6k,
        (double v) => _currentEqualizer.copyWith(hz6k: v),
      ),
      (
        '12kHz',
        _currentEqualizer.hz12k,
        (double v) => _currentEqualizer.copyWith(hz12k: v),
      ),
      (
        '14kHz',
        _currentEqualizer.hz14k,
        (double v) => _currentEqualizer.copyWith(hz14k: v),
      ),
      (
        '16kHz',
        _currentEqualizer.hz16k,
        (double v) => _currentEqualizer.copyWith(hz16k: v),
      ),
    ];

    return Card(
      elevation: 2,
      child: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(
                  '10-Band Equalizer',
                  style: Theme.of(context).textTheme.titleMedium,
                ),
                DropdownButton<String>(
                  value: null,
                  hint: const Text('Presets'),
                  items: const [
                    DropdownMenuItem(value: 'flat', child: Text('Flat')),
                    DropdownMenuItem(value: 'rock', child: Text('Rock')),
                    DropdownMenuItem(value: 'pop', child: Text('Pop')),
                    DropdownMenuItem(value: 'jazz', child: Text('Jazz')),
                    DropdownMenuItem(
                      value: 'classical',
                      child: Text('Classical'),
                    ),
                    DropdownMenuItem(
                      value: 'bassBoost',
                      child: Text('Bass Boost'),
                    ),
                  ],
                  onChanged: (preset) {
                    if (preset == null) return;
                    Equalizer target;
                    switch (preset) {
                      case 'rock':
                        target = Equalizer.rock;
                      case 'pop':
                        target = Equalizer.pop;
                      case 'jazz':
                        target = Equalizer.jazz;
                      case 'classical':
                        target = Equalizer.classical;
                      case 'bassBoost':
                        target = Equalizer.bassBoost;
                      default:
                        target = Equalizer.flat;
                    }
                    setState(() => _currentEqualizer = target);
                    _player.action.setEqualizer(target);
                  },
                ),
              ],
            ),
            const SizedBox(height: 12),
            SizedBox(
              height: 180,
              child: SingleChildScrollView(
                scrollDirection: Axis.horizontal,
                physics: const BouncingScrollPhysics(),
                child: Row(
                  children: bands.map((band) {
                    return SizedBox(
                      width: 48,
                      child: Column(
                        children: [
                          Text(
                            '${band.$2.toInt() > 0 ? "+" : ""}${band.$2.toInt()}',
                            style: const TextStyle(fontSize: 10),
                          ),
                          Expanded(
                            child: RotatedBox(
                              quarterTurns: 3,
                              child: Slider(
                                value: band.$2.clamp(-24.0, 24.0),
                                min: -24.0,
                                max: 24.0,
                                onChanged: (newVal) {
                                  final updated = band.$3(newVal);
                                  setState(() => _currentEqualizer = updated);
                                  _player.action.setEqualizer(updated);
                                },
                              ),
                            ),
                          ),
                          Text(band.$1, style: const TextStyle(fontSize: 10)),
                        ],
                      ),
                    );
                  }).toList(),
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}
3
likes
160
points
546
downloads

Documentation

API reference

Publisher

verified publisherchomusuke.dev

Weekly Downloads

Ultra-low-resource cross-platform audio player using miniaudio. Equalizer, pitch and rate are supported.

Repository (GitHub)
View/report issues

Topics

#audio #miniaudio #sound #player

License

MIT (license)

Dependencies

code_assets, ffi, hooks, logging, native_toolchain_c

More

Packages that depend on miniaudio_player