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 FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import audio_session
|
||||||
|
import just_audio
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
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"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
80
pubspec.lock
80
pubspec.lock
@@ -9,6 +9,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -41,6 +49,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.19.1"
|
version: "1.19.1"
|
||||||
|
crypto:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: crypto
|
||||||
|
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.7"
|
||||||
cupertino_icons:
|
cupertino_icons:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -73,6 +89,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
version: "7.0.1"
|
||||||
|
fixnum:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fixnum
|
||||||
|
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
flutter:
|
flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -109,6 +133,30 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.20.2"
|
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:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -237,6 +285,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
rxdart:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rxdart
|
||||||
|
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.28.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -330,6 +386,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.1"
|
version: "1.4.1"
|
||||||
|
synchronized:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: synchronized
|
||||||
|
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.4.0"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -346,6 +410,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.6"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ dependencies:
|
|||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
path_provider: ^2.1.4
|
path_provider: ^2.1.4
|
||||||
shared_preferences: ^2.3.1
|
shared_preferences: ^2.3.1
|
||||||
|
just_audio: ^0.9.42
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
@@ -65,6 +66,8 @@ flutter:
|
|||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
assets:
|
||||||
- assets/
|
- assets/
|
||||||
|
- assets/audio/bgm/
|
||||||
|
- assets/audio/sfx/
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|||||||
Reference in New Issue
Block a user