feat(audio): 오디오 시스템 추가

- just_audio 패키지 추가
- AudioService 구현 (BGM/SFX 재생)
- assets/audio/bgm/, assets/audio/sfx/ 에셋 추가
This commit is contained in:
JiWoong Sul
2025-12-30 14:22:21 +09:00
parent 162a09c54a
commit 7d19905c01
15 changed files with 315 additions and 0 deletions

BIN
assets/audio/bgm/battle.wav Normal file

Binary file not shown.

BIN
assets/audio/bgm/boss.mp3 Normal file

Binary file not shown.

BIN
assets/audio/bgm/town.mp3 Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/audio/sfx/attack.wav Normal file

Binary file not shown.

BIN
assets/audio/sfx/click.wav Normal file

Binary file not shown.

BIN
assets/audio/sfx/hit.wav Normal file

Binary file not shown.

BIN
assets/audio/sfx/item.wav Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
assets/audio/sfx/skill.wav Normal file

Binary file not shown.

View 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',
};
}

View File

@@ -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"))
} }

View File

@@ -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:

View File

@@ -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