Files
flame_flip/lib/audio/audio_manager.dart

289 lines
6.7 KiB
Dart
Raw Normal View History

2026-06-10 18:23:39 +08:00
import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';
import '../game/music.dart';
class AudioManager {
static final AudioManager instance = AudioManager._();
AudioManager._();
late final AudioPlayer _bgm = AudioPlayer(playerId: 'bgm');
final List<AudioPlayer> _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 = 6;
Future<void> 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<void> _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<void> _initSfxPlayers() async {
for (int i = 0; i < _sfxPoolSize; i++) {
final player = AudioPlayer(playerId: 'sfx_$i');
await _safeSetSfxPlayerDefaults(player, index: i);
_sfxPlayers.add(player);
}
}
Future<void> _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<void> _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<void> setBgmMuted(bool muted) async {
await _ensureInit();
_bgmMuted = muted;
if (muted) {
await _bgm.pause();
} else {
await _bgm.resume();
}
}
Future<void> setSfxMuted(bool muted) async {
await _ensureInit();
_sfxMuted = muted;
if (muted) {
for (final p in _sfxPlayers) {
try {
await p.stop();
} catch (_) {}
}
}
}
Future<void> setBgmVolume(double value) async {
await _ensureInit();
_bgmVolume = value.clamp(0.0, 1.0);
await _bgm.setVolume(_bgmVolume);
}
Future<void> 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<void> 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<void> pauseBgm() async {
if (!_initialized) return;
await _bgm.pause();
}
Future<void> resumeBgm() async {
await _ensureInit();
if (_bgmMuted) return;
await _bgm.resume();
}
Future<void> stopBgm() async {
if (!_initialized) return;
await _bgm.stop();
}
Future<void> 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<void> stopAllSfx() async {
if (!_initialized) return;
for (final p in _sfxPlayers) {
try {
await p.stop();
} catch (_) {}
}
}
Future<void> 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;
}
}