audio_manager.dart 6.9 KB

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