|
|
@@ -2,6 +2,7 @@ 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();
|
|
|
@@ -9,7 +10,7 @@ class AudioManager {
|
|
|
AudioManager._internal();
|
|
|
|
|
|
final Random _random = Random();
|
|
|
-
|
|
|
+
|
|
|
bool _soundEnabled = true;
|
|
|
bool _musicEnabled = true;
|
|
|
bool _hapticEnabled = true;
|
|
|
@@ -21,6 +22,20 @@ class AudioManager {
|
|
|
AudioPool? _popPool;
|
|
|
AudioPool? _popAltPool;
|
|
|
|
|
|
+ // Current playing music context
|
|
|
+ String? _currentMenuMusic;
|
|
|
+ String? _currentIngameMusic;
|
|
|
+ bool _isMenuMusic = false;
|
|
|
+
|
|
|
+ // Available ingame music tracks by theme
|
|
|
+ static const Map<String, List<String>> _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;
|
|
|
@@ -31,16 +46,33 @@ class AudioManager {
|
|
|
|
|
|
Future<void> 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'
|
|
|
+ '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 = <String>[];
|
|
|
+ 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',
|
|
|
@@ -54,18 +86,20 @@ class AudioManager {
|
|
|
print('Failed to load audio assets or create pools: $e');
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
print('AudioManager initialized for ${Platform.operatingSystem}');
|
|
|
}
|
|
|
|
|
|
Future<void> _detectPlatformCapabilities() async {
|
|
|
// Check audio support
|
|
|
_isAudioSupported = _checkAudioSupport();
|
|
|
-
|
|
|
+
|
|
|
// Check vibration support
|
|
|
_isVibrationSupported = _checkVibrationSupport();
|
|
|
-
|
|
|
- print('Platform capabilities - Audio: $_isAudioSupported, Vibration: $_isVibrationSupported');
|
|
|
+
|
|
|
+ print(
|
|
|
+ 'Platform capabilities - Audio: $_isAudioSupported, Vibration: $_isVibrationSupported',
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
bool _checkAudioSupport() {
|
|
|
@@ -94,26 +128,28 @@ class AudioManager {
|
|
|
|
|
|
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';
|
|
|
+ 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) {
|
|
|
+ } else if (sound == 'source/bubble_pop_alt.mp3' &&
|
|
|
+ _popAltPool != null) {
|
|
|
_popAltPool!.start(volume: _sfxVolume);
|
|
|
} else {
|
|
|
// Fallback to direct playback if pools fail
|
|
|
@@ -127,7 +163,7 @@ class AudioManager {
|
|
|
_playSystemSoundAsync();
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
void _playSystemSoundAsync() async {
|
|
|
try {
|
|
|
// Vary the system sound for some variety
|
|
|
@@ -146,13 +182,13 @@ class AudioManager {
|
|
|
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();
|
|
|
@@ -163,33 +199,91 @@ class AudioManager {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ Future<void> 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';
|
|
|
|
|
|
- Future<void> playBackgroundMusic() async {
|
|
|
+ // 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<void> playIngameMusic() async {
|
|
|
if (!_musicEnabled || !_isAudioSupported) {
|
|
|
if (!_isAudioSupported) {
|
|
|
- print('Background music not available - using system audio only');
|
|
|
+ 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 with updated volume
|
|
|
- FlameAudio.bgm.play(
|
|
|
- 'source/ambient_background.mp3',
|
|
|
- volume: _bgmVolume
|
|
|
- );
|
|
|
- print('Background music started');
|
|
|
+
|
|
|
+ // 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 background music: $e');
|
|
|
+ print('Failed to play ingame music: $e');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ Future<void> playBackgroundMusic() async {
|
|
|
+ // Fallback method - defaults to menu music for compatibility
|
|
|
+ await playMenuMusic();
|
|
|
+ }
|
|
|
+
|
|
|
Future<void> stopBackgroundMusic() async {
|
|
|
if (!_isAudioSupported) return;
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
FlameAudio.bgm.stop();
|
|
|
} catch (e) {
|
|
|
@@ -199,7 +293,7 @@ class AudioManager {
|
|
|
|
|
|
Future<void> pauseBackgroundMusic() async {
|
|
|
if (!_isAudioSupported) return;
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
FlameAudio.bgm.pause();
|
|
|
} catch (e) {
|
|
|
@@ -209,14 +303,13 @@ class AudioManager {
|
|
|
|
|
|
Future<void> 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;
|
|
|
@@ -227,7 +320,13 @@ class AudioManager {
|
|
|
if (!enabled) {
|
|
|
stopBackgroundMusic();
|
|
|
} else {
|
|
|
- playBackgroundMusic();
|
|
|
+ // Resume appropriate music based on context
|
|
|
+ if (_isMenuMusic ||
|
|
|
+ (_currentMenuMusic != null && _currentIngameMusic == null)) {
|
|
|
+ playMenuMusic();
|
|
|
+ } else {
|
|
|
+ playIngameMusic();
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -251,13 +350,13 @@ class AudioManager {
|
|
|
} catch (e) {
|
|
|
print('Failed to dispose background player: $e');
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
await _popPool?.dispose();
|
|
|
} catch (e) {
|
|
|
print('Failed to dispose popPool: $e');
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
try {
|
|
|
await _popAltPool?.dispose();
|
|
|
} catch (e) {
|
|
|
@@ -267,9 +366,10 @@ class AudioManager {
|
|
|
|
|
|
// Utility method to get platform info for debugging
|
|
|
String getPlatformInfo() {
|
|
|
- final audioType = _isAudioSupported ? 'Full Audio Support' : 'System Sounds Only';
|
|
|
+ final audioType =
|
|
|
+ _isAudioSupported ? 'Full Audio Support' : 'System Sounds Only';
|
|
|
return 'Platform: ${Platform.operatingSystem}, '
|
|
|
- 'Audio: $audioType, '
|
|
|
- 'Vibration: $_isVibrationSupported';
|
|
|
+ 'Audio: $audioType, '
|
|
|
+ 'Vibration: $_isVibrationSupported';
|
|
|
}
|
|
|
-}
|
|
|
+}
|