feat(audio): 오디오 시스템 추가
- just_audio 패키지 추가 - AudioService 구현 (BGM/SFX 재생) - assets/audio/bgm/, assets/audio/sfx/ 에셋 추가
This commit is contained in:
BIN
assets/audio/bgm/battle.wav
Normal file
BIN
assets/audio/bgm/battle.wav
Normal file
Binary file not shown.
BIN
assets/audio/bgm/boss.mp3
Normal file
BIN
assets/audio/bgm/boss.mp3
Normal file
Binary file not shown.
BIN
assets/audio/bgm/town.mp3
Normal file
BIN
assets/audio/bgm/town.mp3
Normal file
Binary file not shown.
BIN
assets/audio/bgm/victory.mp3
Normal file
BIN
assets/audio/bgm/victory.mp3
Normal file
Binary file not shown.
BIN
assets/audio/sfx/attack.wav
Normal file
BIN
assets/audio/sfx/attack.wav
Normal file
Binary file not shown.
BIN
assets/audio/sfx/click.wav
Normal file
BIN
assets/audio/sfx/click.wav
Normal file
Binary file not shown.
BIN
assets/audio/sfx/hit.wav
Normal file
BIN
assets/audio/sfx/hit.wav
Normal file
Binary file not shown.
BIN
assets/audio/sfx/item.wav
Normal file
BIN
assets/audio/sfx/item.wav
Normal file
Binary file not shown.
BIN
assets/audio/sfx/level_up.wav
Normal file
BIN
assets/audio/sfx/level_up.wav
Normal file
Binary file not shown.
BIN
assets/audio/sfx/quest_complete.wav
Normal file
BIN
assets/audio/sfx/quest_complete.wav
Normal file
Binary file not shown.
BIN
assets/audio/sfx/skill.wav
Normal file
BIN
assets/audio/sfx/skill.wav
Normal file
Binary file not shown.
228
lib/src/core/audio/audio_service.dart
Normal file
228
lib/src/core/audio/audio_service.dart
Normal file
@@ -0,0 +1,228 @@
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
|
||||
import 'package:askiineverdie/src/core/storage/settings_repository.dart';
|
||||
|
||||
/// 게임 오디오 서비스
|
||||
///
|
||||
/// BGM과 SFX를 관리하며, 설정과 연동하여 볼륨을 조절합니다.
|
||||
class AudioService {
|
||||
AudioService({SettingsRepository? settingsRepository})
|
||||
: _settingsRepository = settingsRepository ?? SettingsRepository();
|
||||
|
||||
final SettingsRepository _settingsRepository;
|
||||
|
||||
// BGM 플레이어
|
||||
AudioPlayer? _bgmPlayer;
|
||||
|
||||
// SFX 플레이어 풀 (동시 재생 지원)
|
||||
final List<AudioPlayer> _sfxPlayers = [];
|
||||
static const int _maxSfxPlayers = 5;
|
||||
|
||||
// 현재 볼륨
|
||||
double _bgmVolume = 0.7;
|
||||
double _sfxVolume = 0.8;
|
||||
|
||||
// 현재 재생 중인 BGM
|
||||
String? _currentBgm;
|
||||
|
||||
// 초기화 여부
|
||||
bool _initialized = false;
|
||||
|
||||
/// 서비스 초기화
|
||||
Future<void> init() async {
|
||||
if (_initialized) return;
|
||||
|
||||
// 설정에서 볼륨 불러오기
|
||||
_bgmVolume = await _settingsRepository.loadBgmVolume();
|
||||
_sfxVolume = await _settingsRepository.loadSfxVolume();
|
||||
|
||||
// BGM 플레이어 초기화
|
||||
_bgmPlayer = AudioPlayer();
|
||||
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);
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// BGM 재생
|
||||
///
|
||||
/// [name]은 assets/audio/bgm/ 폴더 내 파일명 (확장자 제외)
|
||||
/// 예: playBgm('battle') → assets/audio/bgm/battle.wav 또는 battle.mp3
|
||||
Future<void> playBgm(String name) async {
|
||||
if (!_initialized) await init();
|
||||
if (_currentBgm == name) return; // 이미 재생 중
|
||||
|
||||
try {
|
||||
_currentBgm = name;
|
||||
// WAV 먼저 시도, 실패하면 MP3 시도
|
||||
try {
|
||||
await _bgmPlayer!.setAsset('assets/audio/bgm/$name.wav');
|
||||
} catch (_) {
|
||||
await _bgmPlayer!.setAsset('assets/audio/bgm/$name.mp3');
|
||||
}
|
||||
await _bgmPlayer!.play();
|
||||
} catch (e) {
|
||||
// 파일이 없으면 무시 (개발 중 에셋 미추가 상태)
|
||||
_currentBgm = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// BGM 정지
|
||||
Future<void> stopBgm() async {
|
||||
if (!_initialized) return;
|
||||
|
||||
await _bgmPlayer!.stop();
|
||||
_currentBgm = null;
|
||||
}
|
||||
|
||||
/// BGM 일시정지
|
||||
Future<void> pauseBgm() async {
|
||||
if (!_initialized) return;
|
||||
await _bgmPlayer!.pause();
|
||||
}
|
||||
|
||||
/// BGM 재개
|
||||
Future<void> resumeBgm() async {
|
||||
if (!_initialized) return;
|
||||
if (_currentBgm != null) {
|
||||
await _bgmPlayer!.play();
|
||||
}
|
||||
}
|
||||
|
||||
/// SFX 재생
|
||||
///
|
||||
/// [name]은 assets/audio/sfx/ 폴더 내 파일명 (확장자 제외)
|
||||
/// 예: playSfx('attack') → assets/audio/sfx/attack.wav 또는 attack.mp3
|
||||
Future<void> playSfx(String name) async {
|
||||
if (!_initialized) await init();
|
||||
if (_sfxVolume == 0) return; // 볼륨이 0이면 재생 안함
|
||||
|
||||
// 사용 가능한 플레이어 찾기
|
||||
AudioPlayer? availablePlayer;
|
||||
for (final player in _sfxPlayers) {
|
||||
if (!player.playing) {
|
||||
availablePlayer = player;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 모든 플레이어가 사용 중이면 첫 번째 플레이어 재사용
|
||||
availablePlayer ??= _sfxPlayers.first;
|
||||
|
||||
try {
|
||||
// WAV 먼저 시도, 실패하면 MP3 시도
|
||||
try {
|
||||
await availablePlayer.setAsset('assets/audio/sfx/$name.wav');
|
||||
} catch (_) {
|
||||
await availablePlayer.setAsset('assets/audio/sfx/$name.mp3');
|
||||
}
|
||||
await availablePlayer.seek(Duration.zero);
|
||||
await availablePlayer.play();
|
||||
} catch (e) {
|
||||
// 파일이 없으면 무시
|
||||
}
|
||||
}
|
||||
|
||||
/// BGM 볼륨 설정 (0.0 ~ 1.0)
|
||||
Future<void> setBgmVolume(double volume) async {
|
||||
_bgmVolume = volume.clamp(0.0, 1.0);
|
||||
if (_initialized && _bgmPlayer != null) {
|
||||
await _bgmPlayer!.setVolume(_bgmVolume);
|
||||
}
|
||||
await _settingsRepository.saveBgmVolume(_bgmVolume);
|
||||
}
|
||||
|
||||
/// SFX 볼륨 설정 (0.0 ~ 1.0)
|
||||
Future<void> setSfxVolume(double volume) async {
|
||||
_sfxVolume = volume.clamp(0.0, 1.0);
|
||||
if (_initialized) {
|
||||
for (final player in _sfxPlayers) {
|
||||
await player.setVolume(_sfxVolume);
|
||||
}
|
||||
}
|
||||
await _settingsRepository.saveSfxVolume(_sfxVolume);
|
||||
}
|
||||
|
||||
/// 현재 BGM 볼륨
|
||||
double get bgmVolume => _bgmVolume;
|
||||
|
||||
/// 현재 SFX 볼륨
|
||||
double get sfxVolume => _sfxVolume;
|
||||
|
||||
/// 현재 재생 중인 BGM
|
||||
String? get currentBgm => _currentBgm;
|
||||
|
||||
/// 서비스 정리
|
||||
Future<void> dispose() async {
|
||||
await _bgmPlayer?.dispose();
|
||||
for (final player in _sfxPlayers) {
|
||||
await player.dispose();
|
||||
}
|
||||
_sfxPlayers.clear();
|
||||
_initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// BGM 타입 열거형
|
||||
enum BgmType {
|
||||
/// 마을/상점 BGM
|
||||
town,
|
||||
|
||||
/// 일반 전투 BGM
|
||||
battle,
|
||||
|
||||
/// 보스 전투 BGM
|
||||
boss,
|
||||
|
||||
/// 레벨업/퀘스트 완료 팡파레
|
||||
victory,
|
||||
}
|
||||
|
||||
/// SFX 타입 열거형
|
||||
enum SfxType {
|
||||
/// 공격
|
||||
attack,
|
||||
|
||||
/// 피격
|
||||
hit,
|
||||
|
||||
/// 스킬 사용
|
||||
skill,
|
||||
|
||||
/// 아이템 획득
|
||||
item,
|
||||
|
||||
/// UI 클릭
|
||||
click,
|
||||
|
||||
/// 레벨업
|
||||
levelUp,
|
||||
|
||||
/// 퀘스트 완료
|
||||
questComplete,
|
||||
}
|
||||
|
||||
/// BgmType을 파일명으로 변환
|
||||
extension BgmTypeExtension on BgmType {
|
||||
String get fileName => name;
|
||||
}
|
||||
|
||||
/// SfxType을 파일명으로 변환
|
||||
extension SfxTypeExtension on SfxType {
|
||||
String get fileName => switch (this) {
|
||||
SfxType.attack => 'attack',
|
||||
SfxType.hit => 'hit',
|
||||
SfxType.skill => 'skill',
|
||||
SfxType.item => 'item',
|
||||
SfxType.click => 'click',
|
||||
SfxType.levelUp => 'level_up',
|
||||
SfxType.questComplete => 'quest_complete',
|
||||
};
|
||||
}
|
||||
@@ -5,10 +5,14 @@
|
||||
import FlutterMacOS
|
||||
import Foundation
|
||||
|
||||
import audio_session
|
||||
import just_audio
|
||||
import path_provider_foundation
|
||||
import shared_preferences_foundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
|
||||
JustAudioPlugin.register(with: registry.registrar(forPlugin: "JustAudioPlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
}
|
||||
|
||||
80
pubspec.lock
80
pubspec.lock
@@ -9,6 +9,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
audio_session:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: audio_session
|
||||
sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.25"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -41,6 +49,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: crypto
|
||||
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -73,6 +89,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
fixnum:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: fixnum
|
||||
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -109,6 +133,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.20.2"
|
||||
just_audio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: just_audio
|
||||
sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.46"
|
||||
just_audio_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_platform_interface
|
||||
sha256: "2532c8d6702528824445921c5ff10548b518b13f808c2e34c2fd54793b999a6a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
just_audio_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: just_audio_web
|
||||
sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.4.16"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -237,6 +285,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -330,6 +386,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: synchronized
|
||||
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -346,6 +410,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: typed_data
|
||||
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.2"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -37,6 +37,7 @@ dependencies:
|
||||
intl: ^0.20.2
|
||||
path_provider: ^2.1.4
|
||||
shared_preferences: ^2.3.1
|
||||
just_audio: ^0.9.42
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
@@ -65,6 +66,8 @@ flutter:
|
||||
# To add assets to your application, add an assets section, like this:
|
||||
assets:
|
||||
- assets/
|
||||
- assets/audio/bgm/
|
||||
- assets/audio/sfx/
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/to/resolution-aware-images
|
||||
|
||||
Reference in New Issue
Block a user