diff --git a/assets/audio/bgm/battle.wav b/assets/audio/bgm/battle.wav new file mode 100644 index 0000000..3235c83 Binary files /dev/null and b/assets/audio/bgm/battle.wav differ diff --git a/assets/audio/bgm/boss.mp3 b/assets/audio/bgm/boss.mp3 new file mode 100644 index 0000000..cdcee46 Binary files /dev/null and b/assets/audio/bgm/boss.mp3 differ diff --git a/assets/audio/bgm/town.mp3 b/assets/audio/bgm/town.mp3 new file mode 100644 index 0000000..9ef5ef4 Binary files /dev/null and b/assets/audio/bgm/town.mp3 differ diff --git a/assets/audio/bgm/victory.mp3 b/assets/audio/bgm/victory.mp3 new file mode 100644 index 0000000..2d1c168 Binary files /dev/null and b/assets/audio/bgm/victory.mp3 differ diff --git a/assets/audio/sfx/attack.wav b/assets/audio/sfx/attack.wav new file mode 100644 index 0000000..796b3e9 Binary files /dev/null and b/assets/audio/sfx/attack.wav differ diff --git a/assets/audio/sfx/click.wav b/assets/audio/sfx/click.wav new file mode 100644 index 0000000..36c8eec Binary files /dev/null and b/assets/audio/sfx/click.wav differ diff --git a/assets/audio/sfx/hit.wav b/assets/audio/sfx/hit.wav new file mode 100644 index 0000000..e8df0f0 Binary files /dev/null and b/assets/audio/sfx/hit.wav differ diff --git a/assets/audio/sfx/item.wav b/assets/audio/sfx/item.wav new file mode 100644 index 0000000..af5a3e6 Binary files /dev/null and b/assets/audio/sfx/item.wav differ diff --git a/assets/audio/sfx/level_up.wav b/assets/audio/sfx/level_up.wav new file mode 100644 index 0000000..7531dc3 Binary files /dev/null and b/assets/audio/sfx/level_up.wav differ diff --git a/assets/audio/sfx/quest_complete.wav b/assets/audio/sfx/quest_complete.wav new file mode 100644 index 0000000..04e4bff Binary files /dev/null and b/assets/audio/sfx/quest_complete.wav differ diff --git a/assets/audio/sfx/skill.wav b/assets/audio/sfx/skill.wav new file mode 100644 index 0000000..4a41aa4 Binary files /dev/null and b/assets/audio/sfx/skill.wav differ diff --git a/lib/src/core/audio/audio_service.dart b/lib/src/core/audio/audio_service.dart new file mode 100644 index 0000000..c2712e4 --- /dev/null +++ b/lib/src/core/audio/audio_service.dart @@ -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 _sfxPlayers = []; + static const int _maxSfxPlayers = 5; + + // 현재 볼륨 + double _bgmVolume = 0.7; + double _sfxVolume = 0.8; + + // 현재 재생 중인 BGM + String? _currentBgm; + + // 초기화 여부 + bool _initialized = false; + + /// 서비스 초기화 + Future 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 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 stopBgm() async { + if (!_initialized) return; + + await _bgmPlayer!.stop(); + _currentBgm = null; + } + + /// BGM 일시정지 + Future pauseBgm() async { + if (!_initialized) return; + await _bgmPlayer!.pause(); + } + + /// BGM 재개 + Future 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 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 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 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 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', + }; +} diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index b8e2b22..882be97 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) } diff --git a/pubspec.lock b/pubspec.lock index b6e8f67..638b4c8 100644 --- a/pubspec.lock +++ b/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: diff --git a/pubspec.yaml b/pubspec.yaml index 54bfef1..2953737 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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