302 lines
7.1 KiB
Dart
302 lines
7.1 KiB
Dart
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<SfxKey, String> 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<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 = 2;
|
|
|
|
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;
|
|
}
|
|
}
|