feat(audio): SFX 채널 풀 시스템 추가
- SfxChannelPool: 대기열 기반 SFX 재생 시스템 - 채널별 분리로 플레이어/몬스터 사운드 독립 재생 - 모든 사운드의 완전 재생 보장
This commit is contained in:
158
lib/src/core/audio/sfx_channel_pool.dart
Normal file
158
lib/src/core/audio/sfx_channel_pool.dart
Normal file
@@ -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<AudioPlayer> _players = [];
|
||||
|
||||
/// 각 플레이어의 재생 중 여부 추적
|
||||
final List<bool> _playerBusy = [];
|
||||
|
||||
/// 대기열 (재생 대기 중인 에셋 경로)
|
||||
final Queue<String> _pendingQueue = Queue();
|
||||
|
||||
/// 초기화 여부
|
||||
bool _initialized = false;
|
||||
|
||||
/// 초기화 실패 여부
|
||||
bool _initFailed = false;
|
||||
|
||||
/// 현재 볼륨
|
||||
double get volume => _volume;
|
||||
|
||||
/// 초기화
|
||||
Future<void> 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<void> 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<void> setVolume(double volume) async {
|
||||
_volume = volume.clamp(0.0, 1.0);
|
||||
if (_initialized) {
|
||||
for (final player in _players) {
|
||||
await player.setVolume(_volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 리소스 해제
|
||||
Future<void> 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<void> _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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user