first commit
This commit is contained in:
288
lib/audio/audio_manager.dart
Normal file
288
lib/audio/audio_manager.dart
Normal file
@@ -0,0 +1,288 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user