From 764a8353fb7266eae84679d13c27ed8f3196cab4 Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 31 Dec 2025 01:33:10 +0900 Subject: [PATCH] =?UTF-8?q?refactor(audio):=20AudioService=EC=97=90=20?= =?UTF-8?q?=EC=B1=84=EB=84=90=20=ED=92=80=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 단일 SFX 풀을 Player/Monster 채널로 분리 - playPlayerSfx(), playMonsterSfx() 메서드 추가 - playSfx()는 레거시 호환용으로 유지 - pauseAll() 간소화 (채널 풀 자동 완료) --- lib/src/core/audio/audio_service.dart | 126 +++++++++++++++----------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/lib/src/core/audio/audio_service.dart b/lib/src/core/audio/audio_service.dart index 5463111..9409bf4 100644 --- a/lib/src/core/audio/audio_service.dart +++ b/lib/src/core/audio/audio_service.dart @@ -1,12 +1,18 @@ import 'package:flutter/foundation.dart' show debugPrint, kIsWeb; 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'; /// 게임 오디오 서비스 /// /// BGM과 SFX를 관리하며, 설정과 연동하여 볼륨을 조절합니다. /// 웹/WASM 환경에서는 제한적으로 작동할 수 있습니다. +/// +/// 채널 구조: +/// - BGM: 단일 플레이어 (루프 재생) +/// - Player SFX: 플레이어 이펙트 (공격, 스킬, 아이템 등) +/// - Monster SFX: 몬스터 이펙트 (몬스터 공격 = 플레이어 피격) class AudioService { AudioService({SettingsRepository? settingsRepository}) : _settingsRepository = settingsRepository ?? SettingsRepository(); @@ -16,9 +22,13 @@ class AudioService { // BGM 플레이어 AudioPlayer? _bgmPlayer; - // SFX 플레이어 풀 (동시 재생 지원) - final List _sfxPlayers = []; - static const int _maxSfxPlayers = 5; + // SFX 채널 풀 (채널별 분리, 완료 보장) + SfxChannelPool? _playerSfxPool; + SfxChannelPool? _monsterSfxPool; + + // 채널별 풀 크기 + static const int _playerPoolSize = 4; + static const int _monsterPoolSize = 3; // 현재 볼륨 double _bgmVolume = 0.7; @@ -56,12 +66,20 @@ class AudioService { await _bgmPlayer!.setLoopMode(LoopMode.one); await _bgmPlayer!.setVolume(_bgmVolume); - // SFX 플레이어 풀 초기화 - for (var i = 0; i < _maxSfxPlayers; i++) { - final player = AudioPlayer(); - await player.setVolume(_sfxVolume); - _sfxPlayers.add(player); - } + // SFX 채널 풀 초기화 (채널별 분리) + _playerSfxPool = SfxChannelPool( + name: 'Player', + poolSize: _playerPoolSize, + volume: _sfxVolume, + ); + await _playerSfxPool!.init(); + + _monsterSfxPool = SfxChannelPool( + name: 'Monster', + poolSize: _monsterPoolSize, + volume: _sfxVolume, + ); + await _monsterSfxPool!.init(); _initialized = true; @@ -143,10 +161,8 @@ class AudioService { await _bgmPlayer?.stop(); _currentBgm = null; - // 모든 SFX 정지 - for (final player in _sfxPlayers) { - await player.stop(); - } + // SFX 채널 풀은 자동 완료되므로 별도 정지 불필요 + // (새로운 재생 요청만 _isPaused로 차단) debugPrint('[AudioService] All audio paused'); } @@ -159,51 +175,60 @@ class AudioService { debugPrint('[AudioService] Audio resumed'); } - /// SFX 재생 + /// 플레이어 이펙트 SFX 재생 /// /// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외) - /// 예: playSfx('attack') → assets/audio/sfx/attack.mp3 + /// 예: playPlayerSfx('attack') → assets/audio/sfx/attack.mp3 /// - /// 웹에서 대기 중인 BGM이 있으면 함께 재생 시작합니다. - Future playSfx(String name) async { - if (_initFailed) return; // 초기화 실패 시 무시 - if (_isPaused) return; // 일시정지 상태면 무시 + /// 대기열 기반으로 모든 사운드의 완전 재생을 보장합니다. + Future playPlayerSfx(String name) async { + if (_initFailed) return; + if (_isPaused) return; if (!_initialized) await init(); 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 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) { _userInteracted = true; final pending = _pendingBgm; _pendingBgm = null; - // BGM 재생 (비동기로 진행) 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 playSfx(String name) => playPlayerSfx(name); + /// BGM 볼륨 설정 (0.0 ~ 1.0) Future setBgmVolume(double volume) async { _bgmVolume = volume.clamp(0.0, 1.0); @@ -214,12 +239,13 @@ class AudioService { } /// SFX 볼륨 설정 (0.0 ~ 1.0) + /// + /// 모든 SFX 채널 (플레이어, 몬스터)에 동시 적용됩니다. Future setSfxVolume(double volume) async { _sfxVolume = volume.clamp(0.0, 1.0); if (_initialized) { - for (final player in _sfxPlayers) { - await player.setVolume(_sfxVolume); - } + await _playerSfxPool?.setVolume(_sfxVolume); + await _monsterSfxPool?.setVolume(_sfxVolume); } await _settingsRepository.saveSfxVolume(_sfxVolume); } @@ -251,10 +277,8 @@ class AudioService { /// 서비스 정리 Future dispose() async { await _bgmPlayer?.dispose(); - for (final player in _sfxPlayers) { - await player.dispose(); - } - _sfxPlayers.clear(); + await _playerSfxPool?.dispose(); + await _monsterSfxPool?.dispose(); _initialized = false; } }