audio_manager.dart 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import 'package:flutter/services.dart';
  2. import 'package:flame_audio/flame_audio.dart';
  3. import 'dart:math';
  4. import 'dart:io' show Platform;
  5. class AudioManager {
  6. static final AudioManager _instance = AudioManager._internal();
  7. factory AudioManager() => _instance;
  8. AudioManager._internal();
  9. final Random _random = Random();
  10. bool _soundEnabled = true;
  11. bool _musicEnabled = true;
  12. bool _hapticEnabled = true;
  13. double _bgmVolume = 0.4; // 40% default
  14. double _sfxVolume = 0.6; // 60% default
  15. bool _isAudioSupported = false;
  16. bool _isVibrationSupported = false;
  17. AudioPool? _popPool;
  18. AudioPool? _popAltPool;
  19. bool get soundEnabled => _soundEnabled;
  20. bool get musicEnabled => _musicEnabled;
  21. bool get hapticEnabled => _hapticEnabled;
  22. double get bgmVolume => _bgmVolume;
  23. double get sfxVolume => _sfxVolume;
  24. bool get isAudioSupported => _isAudioSupported;
  25. bool get isVibrationSupported => _isVibrationSupported;
  26. Future<void> initialize() async {
  27. await _detectPlatformCapabilities();
  28. if (_isAudioSupported) {
  29. // Preload audio assets
  30. try {
  31. await FlameAudio.audioCache.loadAll([
  32. 'source/ambient_background.mp3',
  33. 'source/bubble_pop.mp3',
  34. 'source/bubble_pop_alt.mp3'
  35. ]);
  36. // Create sound pools for efficient playback
  37. _popPool = await FlameAudio.createPool(
  38. 'source/bubble_pop.mp3',
  39. maxPlayers: 5,
  40. );
  41. _popAltPool = await FlameAudio.createPool(
  42. 'source/bubble_pop_alt.mp3',
  43. maxPlayers: 5,
  44. );
  45. } catch (e) {
  46. print('Failed to load audio assets or create pools: $e');
  47. }
  48. }
  49. print('AudioManager initialized for ${Platform.operatingSystem}');
  50. }
  51. Future<void> _detectPlatformCapabilities() async {
  52. // Check audio support
  53. _isAudioSupported = _checkAudioSupport();
  54. // Check vibration support
  55. _isVibrationSupported = _checkVibrationSupport();
  56. print('Platform capabilities - Audio: $_isAudioSupported, Vibration: $_isVibrationSupported');
  57. }
  58. bool _checkAudioSupport() {
  59. try {
  60. // Just Audio supports Android, iOS, web, macOS, Windows and Linux
  61. // See: https://pub.dev/packages/just_audio
  62. return true;
  63. } catch (e) {
  64. print('Audio detection failed: $e');
  65. return false;
  66. }
  67. }
  68. bool _checkVibrationSupport() {
  69. try {
  70. // Only Android and iOS support vibration via HapticFeedback
  71. if (Platform.isAndroid || Platform.isIOS) {
  72. return true;
  73. }
  74. return false;
  75. } catch (e) {
  76. print('Vibration detection failed: $e');
  77. return false;
  78. }
  79. }
  80. Future<void> playBubblePop() async {
  81. if (!_soundEnabled) return;
  82. if (_isAudioSupported) {
  83. // Use Flame Audio for bubble pop
  84. final sound = _random.nextBool()
  85. ? 'source/bubble_pop.mp3'
  86. : 'source/bubble_pop_alt.mp3';
  87. try {
  88. if (sound == 'source/bubble_pop.mp3' && _popPool != null) {
  89. _popPool!.start(volume: _sfxVolume);
  90. } else if (sound == 'source/bubble_pop_alt.mp3' && _popAltPool != null) {
  91. _popAltPool!.start(volume: _sfxVolume);
  92. } else {
  93. // Fallback to direct playback if pools fail
  94. FlameAudio.play(sound, volume: _sfxVolume);
  95. }
  96. } catch (e) {
  97. print('Failed to play bubble pop sound: $e');
  98. }
  99. } else {
  100. // Fallback to system sound
  101. await _playSystemSound();
  102. }
  103. if (_hapticEnabled && _isVibrationSupported) {
  104. await _playHapticFeedback();
  105. }
  106. }
  107. Future<void> _playSystemSound() async {
  108. try {
  109. // Vary the system sound for some variety
  110. final soundType = _random.nextInt(3);
  111. switch (soundType) {
  112. case 0:
  113. await SystemSound.play(SystemSoundType.click);
  114. break;
  115. case 1:
  116. await SystemSound.play(SystemSoundType.alert);
  117. break;
  118. default:
  119. await SystemSound.play(SystemSoundType.click);
  120. }
  121. } catch (e) {
  122. print('Failed to play system sound: $e');
  123. }
  124. }
  125. Future<void> playBackgroundMusic() async {
  126. if (!_musicEnabled || !_isAudioSupported) {
  127. if (!_isAudioSupported) {
  128. print('Background music not available - using system audio only');
  129. }
  130. return;
  131. }
  132. try {
  133. // Stop any existing BGM first
  134. await FlameAudio.bgm.stop();
  135. // Play with updated volume
  136. FlameAudio.bgm.play(
  137. 'source/ambient_background.mp3',
  138. volume: _bgmVolume
  139. );
  140. print('Background music started');
  141. } catch (e) {
  142. print('Failed to play background music: $e');
  143. }
  144. }
  145. Future<void> stopBackgroundMusic() async {
  146. if (!_isAudioSupported) return;
  147. try {
  148. FlameAudio.bgm.stop();
  149. } catch (e) {
  150. print('Failed to stop background music: $e');
  151. }
  152. }
  153. Future<void> pauseBackgroundMusic() async {
  154. if (!_isAudioSupported) return;
  155. try {
  156. FlameAudio.bgm.pause();
  157. } catch (e) {
  158. print('Failed to pause background music: $e');
  159. }
  160. }
  161. Future<void> resumeBackgroundMusic() async {
  162. if (!_musicEnabled || !_isAudioSupported) return;
  163. try {
  164. FlameAudio.bgm.resume();
  165. } catch (e) {
  166. print('Failed to resume background music: $e');
  167. }
  168. }
  169. Future<void> _playHapticFeedback() async {
  170. if (!_isVibrationSupported) {
  171. // Silently skip haptic feedback on unsupported platforms
  172. return;
  173. }
  174. try {
  175. // Use Flutter's built-in HapticFeedback instead of vibration plugin
  176. await HapticFeedback.lightImpact();
  177. } catch (e) {
  178. print('Failed to play haptic feedback: $e');
  179. // Disable vibration support if it fails
  180. _isVibrationSupported = false;
  181. }
  182. }
  183. void setSoundEnabled(bool enabled) {
  184. _soundEnabled = enabled;
  185. }
  186. void setMusicEnabled(bool enabled) {
  187. _musicEnabled = enabled;
  188. if (!enabled) {
  189. stopBackgroundMusic();
  190. } else {
  191. playBackgroundMusic();
  192. }
  193. }
  194. void setBgmVolume(double volume) {
  195. _bgmVolume = volume;
  196. // Volume will be applied when music is next played
  197. }
  198. void setSfxVolume(double volume) {
  199. _sfxVolume = volume;
  200. // No effect with system audio
  201. }
  202. void setHapticEnabled(bool enabled) {
  203. _hapticEnabled = enabled;
  204. }
  205. Future<void> dispose() async {
  206. try {
  207. await FlameAudio.bgm.dispose();
  208. } catch (e) {
  209. print('Failed to dispose background player: $e');
  210. }
  211. try {
  212. await _popPool?.dispose();
  213. } catch (e) {
  214. print('Failed to dispose popPool: $e');
  215. }
  216. try {
  217. await _popAltPool?.dispose();
  218. } catch (e) {
  219. print('Failed to dispose popAltPool: $e');
  220. }
  221. }
  222. // Utility method to get platform info for debugging
  223. String getPlatformInfo() {
  224. final audioType = _isAudioSupported ? 'Full Audio Support' : 'System Sounds Only';
  225. return 'Platform: ${Platform.operatingSystem}, '
  226. 'Audio: $audioType, '
  227. 'Vibration: $_isVibrationSupported';
  228. }
  229. }