From 43289ac848320110113b6e5a2f40a7a4e1d16cad Mon Sep 17 00:00:00 2001 From: JiWoong Sul Date: Wed, 31 Dec 2025 01:33:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(audio):=20SFX=20=EC=B1=84=EB=84=90=20?= =?UTF-8?q?=ED=92=80=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SfxChannelPool: 대기열 기반 SFX 재생 시스템 - 채널별 분리로 플레이어/몬스터 사운드 독립 재생 - 모든 사운드의 완전 재생 보장 --- lib/src/core/audio/sfx_channel_pool.dart | 158 +++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 lib/src/core/audio/sfx_channel_pool.dart diff --git a/lib/src/core/audio/sfx_channel_pool.dart b/lib/src/core/audio/sfx_channel_pool.dart new file mode 100644 index 0000000..f432340 --- /dev/null +++ b/lib/src/core/audio/sfx_channel_pool.dart @@ -0,0 +1,158 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart' show debugPrint; +import 'package:just_audio/just_audio.dart'; + +/// SFX 채널 풀 - 사운드 완료 보장 +/// +/// 대기열 기반으로 모든 사운드의 완전 재생을 보장합니다. +/// 사용 가능한 플레이어가 없으면 대기열에 추가하고, +/// 재생 완료 시 대기열의 다음 사운드를 자동 재생합니다. +class SfxChannelPool { + SfxChannelPool({ + required this.name, + this.poolSize = 4, + double volume = 0.8, + }) : _volume = volume; + + /// 채널명 (디버그용) + final String name; + + /// 풀 크기 + final int poolSize; + + /// 채널 볼륨 + double _volume; + + /// AudioPlayer 풀 + final List _players = []; + + /// 각 플레이어의 재생 중 여부 추적 + final List _playerBusy = []; + + /// 대기열 (재생 대기 중인 에셋 경로) + final Queue _pendingQueue = Queue(); + + /// 초기화 여부 + bool _initialized = false; + + /// 초기화 실패 여부 + bool _initFailed = false; + + /// 현재 볼륨 + double get volume => _volume; + + /// 초기화 + Future init() async { + if (_initialized || _initFailed) return; + + try { + for (var i = 0; i < poolSize; i++) { + final player = AudioPlayer(); + await player.setVolume(_volume); + + // 재생 완료 리스너 등록 + player.playerStateStream.listen((state) { + if (state.processingState == ProcessingState.completed) { + _onPlayerComplete(_players.indexOf(player)); + } + }); + + _players.add(player); + _playerBusy.add(false); + } + + _initialized = true; + debugPrint('[SfxChannelPool:$name] Initialized with $poolSize players'); + } catch (e) { + _initFailed = true; + debugPrint('[SfxChannelPool:$name] Init failed: $e'); + } + } + + /// 사운드 재생 (완료 보장) + /// + /// 사용 가능한 플레이어가 있으면 즉시 재생하고, + /// 모든 플레이어가 사용 중이면 대기열에 추가합니다. + Future play(String assetPath) async { + if (_initFailed) return; + if (!_initialized) await init(); + if (_initFailed || !_initialized) return; + if (_volume == 0) return; // 볼륨이 0이면 재생 안함 + + // 사용 가능한 플레이어 찾기 + final availableIndex = _findAvailablePlayer(); + + if (availableIndex != -1) { + // 즉시 재생 + await _playOnPlayer(availableIndex, assetPath); + } else { + // 대기열에 추가 + _pendingQueue.add(assetPath); + debugPrint('[SfxChannelPool:$name] Queued: $assetPath ' + '(queue size: ${_pendingQueue.length})'); + } + } + + /// 볼륨 설정 (0.0 ~ 1.0) + Future setVolume(double volume) async { + _volume = volume.clamp(0.0, 1.0); + if (_initialized) { + for (final player in _players) { + await player.setVolume(_volume); + } + } + } + + /// 리소스 해제 + Future dispose() async { + for (final player in _players) { + await player.dispose(); + } + _players.clear(); + _playerBusy.clear(); + _pendingQueue.clear(); + _initialized = false; + } + + /// 사용 가능한 플레이어 인덱스 반환 (-1: 없음) + int _findAvailablePlayer() { + for (var i = 0; i < _playerBusy.length; i++) { + if (!_playerBusy[i]) { + return i; + } + } + return -1; + } + + /// 특정 플레이어에서 사운드 재생 + Future _playOnPlayer(int index, String assetPath) async { + if (index < 0 || index >= _players.length) return; + + final player = _players[index]; + _playerBusy[index] = true; + + try { + await player.setAsset(assetPath); + await player.seek(Duration.zero); + await player.play(); + } catch (e) { + // 파일이 없거나 오류 시 busy 상태 해제 + _playerBusy[index] = false; + debugPrint('[SfxChannelPool:$name] Play failed: $assetPath - $e'); + } + } + + /// 플레이어 재생 완료 시 호출 + void _onPlayerComplete(int index) { + if (index < 0 || index >= _playerBusy.length) return; + + _playerBusy[index] = false; + + // 대기열에 항목이 있으면 다음 사운드 재생 + if (_pendingQueue.isNotEmpty) { + final nextAsset = _pendingQueue.removeFirst(); + _playOnPlayer(index, nextAsset); + } + } +}