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