import 'dart:async'; import 'package:just_audio/just_audio.dart'; import 'package:audio_session/audio_session.dart'; import '../models/track_model.dart'; import '../../core/enums/playback_mode.dart'; class AudioService { static final AudioService _instance = AudioService._internal(); factory AudioService() => _instance; AudioService._internal(); late AudioPlayer _audioPlayer; late AudioSession _audioSession; // Playback state TrackModel? _currentTrack; bool _isPlaying = false; bool _isLoading = false; bool _isBuffering = false; Duration _position = Duration.zero; Duration _duration = Duration.zero; double _volume = 1.0; // Playback modes RepeatMode _repeatMode = RepeatMode.off; ShuffleMode _shuffleMode = ShuffleMode.off; double _playbackSpeed = 1.0; // Playlist List _queue = []; int _currentIndex = 0; bool _isShuffleMode = false; bool _isRepeatMode = false; // Error handling String? _errorMessage; void _setError(String error) { _errorMessage = error; _errorController.add(_errorMessage); print('Audio Error: $error'); } void _clearError() { if (_errorMessage != null) { _errorMessage = null; _errorController.add(_errorMessage); } } // Stream controllers final _positionController = StreamController.broadcast(); final _durationController = StreamController.broadcast(); final _playingStateController = StreamController.broadcast(); final _currentTrackController = StreamController.broadcast(); final _queueController = StreamController>.broadcast(); final _bufferingController = StreamController.broadcast(); final _errorController = StreamController.broadcast(); final _repeatModeController = StreamController.broadcast(); final _shuffleModeController = StreamController.broadcast(); // Getters TrackModel? get currentTrack => _currentTrack; bool get isPlaying => _isPlaying; bool get isPaused => !_isPlaying && _currentTrack != null; bool get isLoading => _isLoading; bool get isBuffering => _isBuffering; Duration get position => _position; Duration get duration => _duration; double get volume => _volume; List get queue => _queue; int get currentIndex => _currentIndex; bool get isShuffleMode => _isShuffleMode; bool get isRepeatMode => _isRepeatMode; RepeatMode get repeatMode => _repeatMode; ShuffleMode get shuffleMode => _shuffleMode; double get playbackSpeed => _playbackSpeed; String? get errorMessage => _errorMessage; // Playback state helpers bool get hasError => _errorMessage != null; bool get canPlay => _currentTrack != null && !hasError; bool get canPause => _isPlaying && !hasError; bool get canGoNext => _queue.isNotEmpty && _currentIndex < _queue.length - 1; bool get canGoPrevious => _queue.isNotEmpty && _currentIndex > 0; // Streams Stream get positionStream => _positionController.stream; Stream get durationStream => _durationController.stream; Stream get playingStateStream => _playingStateController.stream; Stream get currentTrackStream => _currentTrackController.stream; Stream> get queueStream => _queueController.stream; Stream get bufferingStream => _bufferingController.stream; Stream get errorStream => _errorController.stream; Stream get repeatModeStream => _repeatModeController.stream; Stream get shuffleModeStream => _shuffleModeController.stream; Future initialize() async { try { _audioPlayer = AudioPlayer(); _audioSession = await AudioSession.instance; // Configure audio session await _audioSession.configure(const AudioSessionConfiguration.music()); // Set up listeners _audioPlayer.positionStream.listen((position) { _position = position; _positionController.add(position); }); _audioPlayer.durationStream.listen((duration) { _duration = duration ?? Duration.zero; _durationController.add(_duration); }); _audioPlayer.playerStateStream.listen((state) { _isPlaying = state.playing; _playingStateController.add(_isPlaying); }); // Handle player completion _audioPlayer.playerStateStream.listen((state) { if (state.processingState == ProcessingState.completed) { _playNext(); } // Handle buffering state _isBuffering = state.processingState == ProcessingState.buffering || state.processingState == ProcessingState.loading; _bufferingController.add(_isBuffering); }); // Handle player errors _audioPlayer.playerStateStream.listen((state) { if (state.playing && _errorMessage != null) { _clearError(); } }); print('Audio service initialized successfully'); } catch (e) { throw Exception('Failed to initialize audio service: $e'); } } Future loadTrack(TrackModel track) async { try { _clearError(); _isLoading = true; _isBuffering = true; _currentTrack = track; _currentTrackController.add(_currentTrack); _bufferingController.add(_isBuffering); // Create audio source from track filepath final uri = Uri.parse(track.filepath); await _audioPlayer.setAudioSource(AudioSource.uri(uri)); _isLoading = false; _isBuffering = false; _bufferingController.add(_isBuffering); print('Track loaded: ${track.title}'); } catch (e) { _isLoading = false; _isBuffering = false; _bufferingController.add(_isBuffering); _setError('Failed to load track: $e'); throw Exception('Failed to load track: $e'); } } Future play() async { try { _clearError(); if (_currentTrack != null && !hasError) { await _audioPlayer.play(); _isPlaying = true; _playingStateController.add(_isPlaying); print('Playing: ${_currentTrack?.title}'); } } catch (e) { _setError('Failed to play: $e'); throw Exception('Failed to play: $e'); } } Future pause() async { try { await _audioPlayer.pause(); _isPlaying = false; _playingStateController.add(_isPlaying); print('Paused: ${_currentTrack?.title}'); } catch (e) { _setError('Failed to pause: $e'); throw Exception('Failed to pause: $e'); } } Future stop() async { try { await _audioPlayer.stop(); _isPlaying = false; _position = Duration.zero; _playingStateController.add(_isPlaying); _positionController.add(_position); _clearError(); print('Stopped: ${_currentTrack?.title}'); } catch (e) { _setError('Failed to stop: $e'); throw Exception('Failed to stop: $e'); } } Future seekTo(Duration position) async { try { await _audioPlayer.seek(position); _position = position; _positionController.add(_position); } catch (e) { throw Exception('Failed to seek: $e'); } } Future setVolume(double volume) async { try { await _audioPlayer.setVolume(volume); } catch (e) { throw Exception('Failed to set volume: $e'); } } Future setSpeed(double speed) async { try { await _audioPlayer.setSpeed(speed); } catch (e) { throw Exception('Failed to set speed: $e'); } } // Queue management void setQueue(List tracks) { _queue = List.from(tracks); _currentIndex = 0; _queueController.add(_queue); if (_queue.isNotEmpty && _currentTrack == null) { loadTrack(_queue[_currentIndex]); } } void addToQueue(TrackModel track) { _queue.add(track); _queueController.add(_queue); } void removeFromQueue(int index) { if (index < _queue.length) { _queue.removeAt(index); if (index < _currentIndex) { _currentIndex--; } else if (index == _currentIndex) { if (_currentIndex >= _queue.length) { _currentIndex = _queue.length - 1; } loadTrack(_queue[_currentIndex]); } _queueController.add(_queue); } } void clearQueue() { _queue.clear(); _currentIndex = 0; _queueController.add(_queue); } Future playNext() async { if (_queue.isNotEmpty) { _playNext(); } } Future playPrevious() async { if (_queue.isNotEmpty) { if (_currentIndex > 0) { _currentIndex--; await loadTrack(_queue[_currentIndex]); await play(); } else if (_repeatMode == RepeatMode.all) { // Loop to last track _currentIndex = _queue.length - 1; await loadTrack(_queue[_currentIndex]); await play(); } else { // Restart current track if at beginning await seekTo(Duration.zero); await play(); } } } void _playNext() { if (_repeatMode == RepeatMode.one) { // Repeat current track loadTrack(_queue[_currentIndex]); play(); } else if (_shuffleMode == ShuffleMode.on) { // Play random track if (_queue.isNotEmpty) { _currentIndex = (_currentIndex + 1) % _queue.length; loadTrack(_queue[_currentIndex]); play(); } } else { // Play next track in order if (_currentIndex < _queue.length - 1) { _currentIndex++; loadTrack(_queue[_currentIndex]); play(); } else if (_repeatMode == RepeatMode.all) { // Loop back to first track _currentIndex = 0; loadTrack(_queue[_currentIndex]); play(); } else { // End of queue stop(); } } } void jumpToIndex(int index) { if (index >= 0 && index < _queue.length) { _currentIndex = index; loadTrack(_queue[_currentIndex]); } } // Playback modes void toggleShuffle() { _shuffleMode = _shuffleMode.toggle(); _shuffleModeController.add(_shuffleMode); if (_shuffleMode == ShuffleMode.on && _queue.isNotEmpty) { // Shuffle the queue while maintaining current track final currentTrack = _queue[_currentIndex]; _queue.shuffle(); _currentIndex = _queue.indexOf(currentTrack); _queueController.add(_queue); } } void toggleRepeat() { _repeatMode = _repeatMode.next(); _repeatModeController.add(_repeatMode); } void setShuffleMode(bool enabled) { _shuffleMode = enabled ? ShuffleMode.on : ShuffleMode.off; _shuffleModeController.add(_shuffleMode); if (_shuffleMode == ShuffleMode.on && _queue.isNotEmpty) { // Shuffle the queue while maintaining current track final currentTrack = _queue[_currentIndex]; _queue.shuffle(); _currentIndex = _queue.indexOf(currentTrack); _queueController.add(_queue); } } void setRepeatMode(RepeatMode mode) { _repeatMode = mode; _repeatModeController.add(_repeatMode); } // Utility methods String get positionFormatted { final minutes = _position.inMinutes; final seconds = _position.inSeconds % 60; return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } String get durationFormatted { final minutes = _duration.inMinutes; final seconds = _duration.inSeconds % 60; return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } double get progress { if (_duration.inMilliseconds == 0) return 0.0; return _position.inMilliseconds / _duration.inMilliseconds; } Future dispose() async { await _positionController.close(); await _durationController.close(); await _playingStateController.close(); await _currentTrackController.close(); await _queueController.close(); await _audioPlayer.dispose(); } }