import 'package:flutter/services.dart'; import 'package:flame_audio/flame_audio.dart'; import 'dart:math'; import 'dart:io' show Platform; import '../../utils/theme_manager.dart'; class AudioManager { static final AudioManager _instance = AudioManager._internal(); factory AudioManager() => _instance; AudioManager._internal(); final Random _random = Random(); bool _soundEnabled = true; bool _musicEnabled = true; bool _hapticEnabled = true; double _bgmVolume = 0.4; // 40% default double _sfxVolume = 0.6; // 60% default bool _isAudioSupported = false; bool _isVibrationSupported = false; AudioPool? _popPool; AudioPool? _popAltPool; // Current playing music context String? _currentMenuMusic; String? _currentIngameMusic; bool _isMenuMusic = false; // Available ingame music tracks by theme static const Map> _ingameMusicTracks = { 'default': ['ingame/ingame_default_001.mp3'], 'spring': ['ingame/ingame_spring_001.mp3'], 'summer': ['ingame/ingame_summer_001.mp3'], 'autumn': ['ingame/ingame_autumn_001.mp3'], 'winter': ['ingame/ingame_winter_001.mp3', 'ingame/ingame_winter_002.mp3'], }; bool get soundEnabled => _soundEnabled; bool get musicEnabled => _musicEnabled; bool get hapticEnabled => _hapticEnabled; double get bgmVolume => _bgmVolume; double get sfxVolume => _sfxVolume; bool get isAudioSupported => _isAudioSupported; bool get isVibrationSupported => _isVibrationSupported; Future initialize() async { await _detectPlatformCapabilities(); if (_isAudioSupported) { // Preload audio assets try { // Basic sound effects await FlameAudio.audioCache.loadAll([ 'source/ambient_background.mp3', 'source/bubble_pop.mp3', 'source/bubble_pop_alt.mp3', ]); // Menu music files await FlameAudio.audioCache.loadAll([ 'menu/menu_default.mp3', 'menu/menu_spring.mp3', 'menu/menu_summer.mp3', 'menu/menu_autumn.mp3', 'menu/menu_winter.mp3', ]); // Ingame music files final allIngameTracks = []; for (final tracks in _ingameMusicTracks.values) { allIngameTracks.addAll(tracks); } await FlameAudio.audioCache.loadAll(allIngameTracks); // Create sound pools for efficient playback _popPool = await FlameAudio.createPool( 'source/bubble_pop.mp3', maxPlayers: 5, ); _popAltPool = await FlameAudio.createPool( 'source/bubble_pop_alt.mp3', maxPlayers: 5, ); } catch (e) { print('Failed to load audio assets or create pools: $e'); } } print('AudioManager initialized for ${Platform.operatingSystem}'); } Future _detectPlatformCapabilities() async { // Check audio support _isAudioSupported = _checkAudioSupport(); // Check vibration support _isVibrationSupported = _checkVibrationSupport(); print( 'Platform capabilities - Audio: $_isAudioSupported, Vibration: $_isVibrationSupported', ); } bool _checkAudioSupport() { try { // Just Audio supports Android, iOS, web, macOS, Windows and Linux // See: https://pub.dev/packages/just_audio return true; } catch (e) { print('Audio detection failed: $e'); return false; } } bool _checkVibrationSupport() { try { // Only Android and iOS support vibration via HapticFeedback if (Platform.isAndroid || Platform.isIOS) { return true; } return false; } catch (e) { print('Vibration detection failed: $e'); return false; } } void playBubblePop() { if (!_soundEnabled) return; // Make audio playback async to avoid blocking main thread _playBubblePopAsync(); // Handle haptic feedback asynchronously if (_hapticEnabled && _isVibrationSupported) { _playHapticFeedbackAsync(); } } void _playBubblePopAsync() async { if (_isAudioSupported) { // Use Flame Audio for bubble pop final sound = _random.nextBool() ? 'source/bubble_pop.mp3' : 'source/bubble_pop_alt.mp3'; try { if (sound == 'source/bubble_pop.mp3' && _popPool != null) { _popPool!.start(volume: _sfxVolume); } else if (sound == 'source/bubble_pop_alt.mp3' && _popAltPool != null) { _popAltPool!.start(volume: _sfxVolume); } else { // Fallback to direct playback if pools fail FlameAudio.play(sound, volume: _sfxVolume); } } catch (e) { print('Failed to play bubble pop sound: $e'); } } else { // Fallback to system sound _playSystemSoundAsync(); } } void _playSystemSoundAsync() async { try { // Vary the system sound for some variety final soundType = _random.nextInt(3); switch (soundType) { case 0: await SystemSound.play(SystemSoundType.click); break; case 1: await SystemSound.play(SystemSoundType.alert); break; default: await SystemSound.play(SystemSoundType.click); } } catch (e) { print('Failed to play system sound: $e'); } } void _playHapticFeedbackAsync() async { if (!_isVibrationSupported) { // Silently skip haptic feedback on unsupported platforms return; } try { // Use Flutter's built-in HapticFeedback instead of vibration plugin await HapticFeedback.lightImpact(); } catch (e) { print('Failed to play haptic feedback: $e'); // Disable vibration support if it fails _isVibrationSupported = false; } } Future playMenuMusic() async { if (!_musicEnabled || !_isAudioSupported) { if (!_isAudioSupported) { print('Menu music not available - using system audio only'); } return; } try { // Get current theme final currentTheme = ThemeManager.effectiveTheme; final themeId = currentTheme.id; final menuMusicPath = 'menu/menu_$themeId.mp3'; // Only change music if it's different from current if (_currentMenuMusic == menuMusicPath && _isMenuMusic) { print('Menu music already playing: $menuMusicPath'); return; } // Stop any existing BGM first await FlameAudio.bgm.stop(); // Play theme-based menu music FlameAudio.bgm.play(menuMusicPath, volume: _bgmVolume); _currentMenuMusic = menuMusicPath; _isMenuMusic = true; _currentIngameMusic = null; print('Menu music started: $menuMusicPath'); } catch (e) { print('Failed to play menu music: $e'); } } Future playIngameMusic() async { if (!_musicEnabled || !_isAudioSupported) { if (!_isAudioSupported) { print('Ingame music not available - using system audio only'); } return; } try { // Get current theme final currentTheme = ThemeManager.effectiveTheme; final themeId = currentTheme.id; // Get available tracks for this theme final availableTracks = _ingameMusicTracks[themeId] ?? _ingameMusicTracks['default']!; // Randomly select a track final selectedTrack = availableTracks[_random.nextInt(availableTracks.length)]; // Only change music if it's different from current if (_currentIngameMusic == selectedTrack && !_isMenuMusic) { print('Ingame music already playing: $selectedTrack'); return; } // Stop any existing BGM first await FlameAudio.bgm.stop(); // Play randomly selected ingame music FlameAudio.bgm.play(selectedTrack, volume: _bgmVolume); _currentIngameMusic = selectedTrack; _isMenuMusic = false; _currentMenuMusic = null; print('Ingame music started: $selectedTrack (theme: $themeId)'); } catch (e) { print('Failed to play ingame music: $e'); } } Future playBackgroundMusic() async { // Fallback method - defaults to menu music for compatibility await playMenuMusic(); } Future stopBackgroundMusic() async { if (!_isAudioSupported) return; try { await FlameAudio.bgm.stop(); _currentMenuMusic = null; _currentIngameMusic = null; _isMenuMusic = false; } catch (e) { print('Failed to stop background music: $e'); } } Future pauseBackgroundMusic() async { if (!_isAudioSupported) return; try { FlameAudio.bgm.pause(); } catch (e) { print('Failed to pause background music: $e'); } } Future resumeBackgroundMusic() async { if (!_musicEnabled || !_isAudioSupported) return; try { FlameAudio.bgm.resume(); } catch (e) { print('Failed to resume background music: $e'); } } void setSoundEnabled(bool enabled) { _soundEnabled = enabled; } void setMusicEnabled(bool enabled) { _musicEnabled = enabled; if (!enabled) { stopBackgroundMusic(); } else { // Resume appropriate music based on context if (_isMenuMusic || (_currentMenuMusic != null && _currentIngameMusic == null)) { playMenuMusic(); } else { playIngameMusic(); } } } void setBgmVolume(double volume) { _bgmVolume = volume; // Volume will be applied when music is next played } void setSfxVolume(double volume) { _sfxVolume = volume; // No effect with system audio } void setHapticEnabled(bool enabled) { _hapticEnabled = enabled; } Future dispose() async { // Stop all audio immediately await stopBackgroundMusic(); try { await FlameAudio.bgm.dispose(); } catch (e) { print('Failed to dispose background player: $e'); } try { await _popPool?.dispose(); _popPool = null; } catch (e) { print('Failed to dispose popPool: $e'); } try { await _popAltPool?.dispose(); _popAltPool = null; } catch (e) { print('Failed to dispose popAltPool: $e'); } // Clear all state _currentMenuMusic = null; _currentIngameMusic = null; _isMenuMusic = false; } // Utility method to get platform info for debugging String getPlatformInfo() { final audioType = _isAudioSupported ? 'Full Audio Support' : 'System Sounds Only'; return 'Platform: ${Platform.operatingSystem}, ' 'Audio: $audioType, ' 'Vibration: $_isVibrationSupported'; } }