refactor(audio): AudioService에 채널 풀 시스템 적용
- 단일 SFX 풀을 Player/Monster 채널로 분리 - playPlayerSfx(), playMonsterSfx() 메서드 추가 - playSfx()는 레거시 호환용으로 유지 - pauseAll() 간소화 (채널 풀 자동 완료)
This commit is contained in:
@@ -1,12 +1,18 @@
|
|||||||
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
import 'package:flutter/foundation.dart' show debugPrint, kIsWeb;
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
|
|
||||||
|
import 'package:askiineverdie/src/core/audio/sfx_channel_pool.dart';
|
||||||
import 'package:askiineverdie/src/core/storage/settings_repository.dart';
|
import 'package:askiineverdie/src/core/storage/settings_repository.dart';
|
||||||
|
|
||||||
/// 게임 오디오 서비스
|
/// 게임 오디오 서비스
|
||||||
///
|
///
|
||||||
/// BGM과 SFX를 관리하며, 설정과 연동하여 볼륨을 조절합니다.
|
/// BGM과 SFX를 관리하며, 설정과 연동하여 볼륨을 조절합니다.
|
||||||
/// 웹/WASM 환경에서는 제한적으로 작동할 수 있습니다.
|
/// 웹/WASM 환경에서는 제한적으로 작동할 수 있습니다.
|
||||||
|
///
|
||||||
|
/// 채널 구조:
|
||||||
|
/// - BGM: 단일 플레이어 (루프 재생)
|
||||||
|
/// - Player SFX: 플레이어 이펙트 (공격, 스킬, 아이템 등)
|
||||||
|
/// - Monster SFX: 몬스터 이펙트 (몬스터 공격 = 플레이어 피격)
|
||||||
class AudioService {
|
class AudioService {
|
||||||
AudioService({SettingsRepository? settingsRepository})
|
AudioService({SettingsRepository? settingsRepository})
|
||||||
: _settingsRepository = settingsRepository ?? SettingsRepository();
|
: _settingsRepository = settingsRepository ?? SettingsRepository();
|
||||||
@@ -16,9 +22,13 @@ class AudioService {
|
|||||||
// BGM 플레이어
|
// BGM 플레이어
|
||||||
AudioPlayer? _bgmPlayer;
|
AudioPlayer? _bgmPlayer;
|
||||||
|
|
||||||
// SFX 플레이어 풀 (동시 재생 지원)
|
// SFX 채널 풀 (채널별 분리, 완료 보장)
|
||||||
final List<AudioPlayer> _sfxPlayers = [];
|
SfxChannelPool? _playerSfxPool;
|
||||||
static const int _maxSfxPlayers = 5;
|
SfxChannelPool? _monsterSfxPool;
|
||||||
|
|
||||||
|
// 채널별 풀 크기
|
||||||
|
static const int _playerPoolSize = 4;
|
||||||
|
static const int _monsterPoolSize = 3;
|
||||||
|
|
||||||
// 현재 볼륨
|
// 현재 볼륨
|
||||||
double _bgmVolume = 0.7;
|
double _bgmVolume = 0.7;
|
||||||
@@ -56,12 +66,20 @@ class AudioService {
|
|||||||
await _bgmPlayer!.setLoopMode(LoopMode.one);
|
await _bgmPlayer!.setLoopMode(LoopMode.one);
|
||||||
await _bgmPlayer!.setVolume(_bgmVolume);
|
await _bgmPlayer!.setVolume(_bgmVolume);
|
||||||
|
|
||||||
// SFX 플레이어 풀 초기화
|
// SFX 채널 풀 초기화 (채널별 분리)
|
||||||
for (var i = 0; i < _maxSfxPlayers; i++) {
|
_playerSfxPool = SfxChannelPool(
|
||||||
final player = AudioPlayer();
|
name: 'Player',
|
||||||
await player.setVolume(_sfxVolume);
|
poolSize: _playerPoolSize,
|
||||||
_sfxPlayers.add(player);
|
volume: _sfxVolume,
|
||||||
}
|
);
|
||||||
|
await _playerSfxPool!.init();
|
||||||
|
|
||||||
|
_monsterSfxPool = SfxChannelPool(
|
||||||
|
name: 'Monster',
|
||||||
|
poolSize: _monsterPoolSize,
|
||||||
|
volume: _sfxVolume,
|
||||||
|
);
|
||||||
|
await _monsterSfxPool!.init();
|
||||||
|
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
|
||||||
@@ -143,10 +161,8 @@ class AudioService {
|
|||||||
await _bgmPlayer?.stop();
|
await _bgmPlayer?.stop();
|
||||||
_currentBgm = null;
|
_currentBgm = null;
|
||||||
|
|
||||||
// 모든 SFX 정지
|
// SFX 채널 풀은 자동 완료되므로 별도 정지 불필요
|
||||||
for (final player in _sfxPlayers) {
|
// (새로운 재생 요청만 _isPaused로 차단)
|
||||||
await player.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
debugPrint('[AudioService] All audio paused');
|
debugPrint('[AudioService] All audio paused');
|
||||||
}
|
}
|
||||||
@@ -159,51 +175,60 @@ class AudioService {
|
|||||||
debugPrint('[AudioService] Audio resumed');
|
debugPrint('[AudioService] Audio resumed');
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SFX 재생
|
/// 플레이어 이펙트 SFX 재생
|
||||||
///
|
///
|
||||||
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
||||||
/// 예: playSfx('attack') → assets/audio/sfx/attack.mp3
|
/// 예: playPlayerSfx('attack') → assets/audio/sfx/attack.mp3
|
||||||
///
|
///
|
||||||
/// 웹에서 대기 중인 BGM이 있으면 함께 재생 시작합니다.
|
/// 대기열 기반으로 모든 사운드의 완전 재생을 보장합니다.
|
||||||
Future<void> playSfx(String name) async {
|
Future<void> playPlayerSfx(String name) async {
|
||||||
if (_initFailed) return; // 초기화 실패 시 무시
|
if (_initFailed) return;
|
||||||
if (_isPaused) return; // 일시정지 상태면 무시
|
if (_isPaused) return;
|
||||||
if (!_initialized) await init();
|
if (!_initialized) await init();
|
||||||
if (_initFailed || !_initialized) return;
|
if (_initFailed || !_initialized) return;
|
||||||
if (_sfxVolume == 0) return; // 볼륨이 0이면 재생 안함
|
|
||||||
if (_sfxPlayers.isEmpty) return;
|
|
||||||
|
|
||||||
// 웹에서 대기 중인 BGM 재생 시도 (사용자 상호작용 발생)
|
// 웹에서 대기 중인 BGM 재생 시도
|
||||||
|
_tryPlayPendingBgm();
|
||||||
|
|
||||||
|
await _playerSfxPool?.play('assets/audio/sfx/$name.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 몬스터 이펙트 SFX 재생
|
||||||
|
///
|
||||||
|
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
||||||
|
/// 예: playMonsterSfx('hit') → assets/audio/sfx/hit.mp3
|
||||||
|
///
|
||||||
|
/// 대기열 기반으로 모든 사운드의 완전 재생을 보장합니다.
|
||||||
|
Future<void> playMonsterSfx(String name) async {
|
||||||
|
if (_initFailed) return;
|
||||||
|
if (_isPaused) return;
|
||||||
|
if (!_initialized) await init();
|
||||||
|
if (_initFailed || !_initialized) return;
|
||||||
|
|
||||||
|
// 웹에서 대기 중인 BGM 재생 시도
|
||||||
|
_tryPlayPendingBgm();
|
||||||
|
|
||||||
|
await _monsterSfxPool?.play('assets/audio/sfx/$name.mp3');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 웹에서 대기 중인 BGM 재생 시도 (사용자 상호작용 발생 시)
|
||||||
|
void _tryPlayPendingBgm() {
|
||||||
if (!_userInteracted && _pendingBgm != null) {
|
if (!_userInteracted && _pendingBgm != null) {
|
||||||
_userInteracted = true;
|
_userInteracted = true;
|
||||||
final pending = _pendingBgm;
|
final pending = _pendingBgm;
|
||||||
_pendingBgm = null;
|
_pendingBgm = null;
|
||||||
// BGM 재생 (비동기로 진행)
|
|
||||||
playBgm(pending!);
|
playBgm(pending!);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 사용 가능한 플레이어 찾기
|
|
||||||
AudioPlayer? availablePlayer;
|
|
||||||
for (final player in _sfxPlayers) {
|
|
||||||
if (!player.playing) {
|
|
||||||
availablePlayer = player;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 모든 플레이어가 사용 중이면 첫 번째 플레이어 재사용
|
|
||||||
availablePlayer ??= _sfxPlayers.first;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await availablePlayer.setAsset('assets/audio/sfx/$name.mp3');
|
|
||||||
await availablePlayer.seek(Duration.zero);
|
|
||||||
await availablePlayer.play();
|
|
||||||
} catch (e) {
|
|
||||||
// 파일이 없으면 무시
|
|
||||||
debugPrint('[AudioService] Failed to play SFX $name: $e');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// SFX 재생 (레거시 호환)
|
||||||
|
///
|
||||||
|
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
||||||
|
/// 예: playSfx('attack') → assets/audio/sfx/attack.mp3
|
||||||
|
///
|
||||||
|
/// @deprecated playPlayerSfx 또는 playMonsterSfx를 사용하세요.
|
||||||
|
Future<void> playSfx(String name) => playPlayerSfx(name);
|
||||||
|
|
||||||
/// BGM 볼륨 설정 (0.0 ~ 1.0)
|
/// BGM 볼륨 설정 (0.0 ~ 1.0)
|
||||||
Future<void> setBgmVolume(double volume) async {
|
Future<void> setBgmVolume(double volume) async {
|
||||||
_bgmVolume = volume.clamp(0.0, 1.0);
|
_bgmVolume = volume.clamp(0.0, 1.0);
|
||||||
@@ -214,12 +239,13 @@ class AudioService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// SFX 볼륨 설정 (0.0 ~ 1.0)
|
/// SFX 볼륨 설정 (0.0 ~ 1.0)
|
||||||
|
///
|
||||||
|
/// 모든 SFX 채널 (플레이어, 몬스터)에 동시 적용됩니다.
|
||||||
Future<void> setSfxVolume(double volume) async {
|
Future<void> setSfxVolume(double volume) async {
|
||||||
_sfxVolume = volume.clamp(0.0, 1.0);
|
_sfxVolume = volume.clamp(0.0, 1.0);
|
||||||
if (_initialized) {
|
if (_initialized) {
|
||||||
for (final player in _sfxPlayers) {
|
await _playerSfxPool?.setVolume(_sfxVolume);
|
||||||
await player.setVolume(_sfxVolume);
|
await _monsterSfxPool?.setVolume(_sfxVolume);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
await _settingsRepository.saveSfxVolume(_sfxVolume);
|
await _settingsRepository.saveSfxVolume(_sfxVolume);
|
||||||
}
|
}
|
||||||
@@ -251,10 +277,8 @@ class AudioService {
|
|||||||
/// 서비스 정리
|
/// 서비스 정리
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _bgmPlayer?.dispose();
|
await _bgmPlayer?.dispose();
|
||||||
for (final player in _sfxPlayers) {
|
await _playerSfxPool?.dispose();
|
||||||
await player.dispose();
|
await _monsterSfxPool?.dispose();
|
||||||
}
|
|
||||||
_sfxPlayers.clear();
|
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user