import 'package:audioplayers/audioplayers.dart'; import 'package:flutter/foundation.dart'; enum SfxKey { win, loss, select, drop, shuffle } enum BgmKey { bgm } class MusicMap { static const String bgm = 'assets/audio/bgm.mp3'; static const Map sfx = { SfxKey.win: 'assets/audio/win.mp3', SfxKey.loss: 'assets/audio/loss.mp3', SfxKey.shuffle: 'assets/audio/shuffle.mp3', SfxKey.select: 'assets/audio/select.mp3', SfxKey.drop: 'assets/audio/drop.mp3', }; } class AudioManager { static final AudioManager instance = AudioManager._(); AudioManager._(); late final AudioPlayer _bgm = AudioPlayer(playerId: 'bgm'); final List _sfxPlayers = []; int _sfxIndex = 0; bool _initialized = false; bool _initializing = false; bool _bgmMuted = false; bool _sfxMuted = false; double _bgmVolume = 0.6; double _sfxVolume = 1.0; static const int _sfxPoolSize = 2; Future init() async { if (_initialized || _initializing) return; _initializing = true; try { _sfxPlayers.clear(); _sfxIndex = 0; await _initBgmPlayer(); await _initSfxPlayers(); if (_sfxPlayers.isEmpty) { final fallback = AudioPlayer(playerId: 'sfx_fallback'); await _safeSetSfxPlayerDefaults(fallback); _sfxPlayers.add(fallback); } _initialized = true; } catch (e, st) { debugPrint('AudioManager init failed: $e'); debugPrint('$st'); _initialized = false; _sfxPlayers.clear(); _sfxIndex = 0; } finally { _initializing = false; } } Future _initBgmPlayer() async { try { await _bgm.setReleaseMode(ReleaseMode.loop); } catch (e) { debugPrint('BGM setReleaseMode failed: $e'); } try { await _bgm.setPlayerMode(PlayerMode.mediaPlayer); } catch (e) { debugPrint('BGM setPlayerMode failed: $e'); } try { await _bgm.setAudioContext( AudioContext( android: AudioContextAndroid( audioFocus: AndroidAudioFocus.gain, contentType: AndroidContentType.music, usageType: AndroidUsageType.media, audioMode: AndroidAudioMode.normal, isSpeakerphoneOn: false, stayAwake: false, ), iOS: AudioContextIOS( category: AVAudioSessionCategory.playback, options: const {AVAudioSessionOptions.mixWithOthers}, ), ), ); } catch (e) { debugPrint('BGM setAudioContext failed: $e'); } try { await _bgm.setVolume(_bgmVolume); } catch (e) { debugPrint('BGM setVolume failed: $e'); } } Future _initSfxPlayers() async { for (int i = 0; i < _sfxPoolSize; i++) { final player = AudioPlayer(playerId: 'sfx_$i'); await _safeSetSfxPlayerDefaults(player, index: i); _sfxPlayers.add(player); } } Future _safeSetSfxPlayerDefaults( AudioPlayer player, { int? index, }) async { final tag = index == null ? 'SFX[fallback]' : 'SFX[$index]'; try { await player.setReleaseMode(ReleaseMode.stop); } catch (e) { debugPrint('$tag setReleaseMode failed: $e'); } try { await player.setPlayerMode(PlayerMode.lowLatency); } catch (e) { debugPrint('$tag lowLatency failed, fallback to mediaPlayer: $e'); try { await player.setPlayerMode(PlayerMode.mediaPlayer); } catch (e2) { debugPrint('$tag mediaPlayer fallback failed: $e2'); } } try { await player.setAudioContext( AudioContext( android: AudioContextAndroid( audioFocus: AndroidAudioFocus.none, contentType: AndroidContentType.sonification, usageType: AndroidUsageType.game, audioMode: AndroidAudioMode.normal, isSpeakerphoneOn: false, stayAwake: false, ), iOS: AudioContextIOS( category: AVAudioSessionCategory.playback, options: const {AVAudioSessionOptions.mixWithOthers}, ), ), ); } catch (e) { debugPrint('$tag setAudioContext failed: $e'); } try { await player.setVolume(_sfxVolume); } catch (e) { debugPrint('$tag setVolume failed: $e'); } } Future _ensureInit() async { if (!_initialized) { await init(); } if (_sfxPlayers.isEmpty) { final fallback = AudioPlayer(playerId: 'sfx_emergency'); await _safeSetSfxPlayerDefaults(fallback); _sfxPlayers.add(fallback); } } AudioPlayer _nextSfxPlayer() { if (_sfxPlayers.isEmpty) { throw StateError('SFX players not initialized'); } final player = _sfxPlayers[_sfxIndex % _sfxPlayers.length]; _sfxIndex = (_sfxIndex + 1) % _sfxPlayers.length; return player; } Future setBgmMuted(bool muted) async { await _ensureInit(); _bgmMuted = muted; if (muted) { await _bgm.pause(); } else { await _bgm.resume(); } } Future setSfxMuted(bool muted) async { await _ensureInit(); _sfxMuted = muted; if (muted) { for (final p in _sfxPlayers) { try { await p.stop(); } catch (_) {} } } } Future setBgmVolume(double value) async { await _ensureInit(); _bgmVolume = value.clamp(0.0, 1.0); await _bgm.setVolume(_bgmVolume); } Future setSfxVolume(double value) async { await _ensureInit(); _sfxVolume = value.clamp(0.0, 1.0); for (final p in _sfxPlayers) { try { await p.setVolume(_sfxVolume); } catch (_) {} } } Future playBgm([BgmKey key = BgmKey.bgm]) async { await _ensureInit(); if (_bgmMuted) return; final path = MusicMap.bgm.replaceFirst('assets/', ''); await _bgm.stop(); await _bgm.play(AssetSource(path), volume: _bgmVolume); } Future pauseBgm() async { if (!_initialized) return; await _bgm.pause(); } Future resumeBgm() async { await _ensureInit(); if (_bgmMuted) return; await _bgm.resume(); } Future stopBgm() async { if (!_initialized) return; await _bgm.stop(); } Future playSfx(SfxKey key) async { await _ensureInit(); if (_sfxMuted) return; if (_sfxPlayers.isEmpty) return; final path = MusicMap.sfx[key]!.replaceFirst('assets/', ''); final player = _nextSfxPlayer(); try { await player.stop(); } catch (_) {} try { await player.setVolume(_sfxVolume); } catch (_) {} try { await player.play(AssetSource(path), volume: _sfxVolume); } catch (e) { debugPrint('playSfx failed: $e'); } } Future stopAllSfx() async { if (!_initialized) return; for (final p in _sfxPlayers) { try { await p.stop(); } catch (_) {} } } Future releaseAll() async { try { await _bgm.dispose(); } catch (_) {} for (final p in _sfxPlayers) { try { await p.dispose(); } catch (_) {} } _sfxPlayers.clear(); _sfxIndex = 0; _initialized = false; _initializing = false; } }