플러터에서 배경음악 연주를 할때 중요한 것은 지연시간입니다.
음악 연주를 하다가 화면이 바뀔때 다른 배경음악을 연주해야 한다면 아래와 같은 현상이 있게 됩니다.
- 재생이 끝나고 다음 재생을 시작할때까지의 지연시간이 생각보다 긴 경우가 있게 됨
이 지연시간을 줄인 패키지가 just_audio입니다. just_audio는 지연시간이 1초보다 작아서 활용에 유리합니다. 지원되는 플랫폼은 Android, iOS, macOS, Web으로 윈도우는 지원을 안하는 것 같애요. 그래도 안드로이드와 iOS가 지원되서 좋습니다.
사용법은 매우 쉬운 편인데요. 함수가 간결하고 직관적입니다. 미디어 플레이어처럼 UI를 세심하게 만들지 않는다면 아래와 같은 함수로도 재생이 됩니다.
- AudioPlayer()로 재생기 객체 생성
- AudioSource.asset()으로 재생할 파일 지정
- ConcatenatingAudioSource()로 여러 파일 연속 재생 소스 설정
- setAudioSource()로 재생 소스를 객체에 넘김
- setLoopMode()로 반복재생 설정
- play() 함수로 재생 시작
- pause() 함수로 재생 잠시 중단
- play() 함수로 이어서 재생 (재생 시작 함수와 같음)
이 기반에서 아래와 같은 소스코드를 작성해볼 수 있습니다.
import 'package:just_audio/just_audio.dart';
class BGMAudioPlayer {
// 오디오 플레이어 인스턴스 생성
static final _player = AudioPlayer();
// MP3 파일 목록 (assets/audio/ 폴더 기준)
static final List<String> _mp3Files = [
'bgm1.mp3',
'bgm2.mp3',
'bgm3.mp3',
'bgm4.mp3',
'bgm5.mp3',
'bgm6.mp3',
'bgm7.mp3',
'bgm8.mp3',
'bgm9.mp3',
'bgm10.mp3',
'bgm11.mp3',
'enka1.mp3',
'enka2.mp3',
'enka3.mp3',
'enka4.mp3',
'versus.mp3',
];
static late final List<String> _path = List<String>.filled(_mp3Files.length, "");
static List<int> index = List<int>.filled(_mp3Files.length, -1);
static shuffleData() {
for (int i = 0; i < _mp3Files.length; i++) {
index[i] = i;
}
index.shuffle();
for (int i = 0; i < _mp3Files.length; i++) {
_path[i] = _mp3Files[index[i]];
}
}
static Future<void> playBackgroundMusic() async {
try {
// 1. 재생 목록 셔플
shuffleData();
// 2. 셔플된 목록으로 연속 재생 소스(Playlist) 생성
final playlist = ConcatenatingAudioSource(
children: _path
.map((fileName) => AudioSource.asset('assets/audios/$fileName'))
.toList(),
);
// 3. 플레이어에 재생 목록 설정
await _player.setAudioSource(playlist);
// 4. 전체 목록 반복 재생 설정
_player.setLoopMode(LoopMode.all);
// 5. 재생 시작
_player.play();
} catch (e) {
// 오류 처리
print("Error occurred while playing audios: $e");
}
}
static Future<void> pauseBackgroundMusic() async {
try {
await _player.pause();
} catch (e) {
print("Error occurred while pausing audios: $e");
}
}
static Future<void> resumeBackgroundMusic() async {
try {
await _player.play();
} catch (e) {
print("Error occurred while pausing audios: $e");
}
}
static Future<void> nextBackgroundMusic() async {
try {
await _player.seekToNext();
await _player.play();
} catch (e) {
print("Error occurred while seek to next audios: $e");
}
}
static Future<void> prevBackgroundMusic() async {
try {
await _player.seekToPrevious();
await _player.play();
} catch (e) {
print("Error occurred while seek to next audios: $e");
}
}
static Future<void> dispose() async {
try {
await _player.dispose();
} catch (e) {
print("Error occurred while diposing AudioPlayer: $e");
}
}
}
이렇게 해두고
@override
void initState() {
…
BGMAudioPlayer.play();
…
}
처럼 기재해서 실행하면 배경음악이 연주되구요.
@override
void dispose() {
// 생명주기 감지기 해제 및 플레이어 dispose (메모리 누수 방지)
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// 앱 생명주기 변경 시 호출되는 메서드
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
// 앱이 다시 활성화되면 음악 재생
BGMAudioPlayer.resumeBackgroundMusic();
break;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
case AppLifecycleState.hidden:
// 앱이 비활성화되거나 백그라운드로 가면 음악 일시정지
BGMAudioPlayer.pauseBackgroundMusic();
break;
case AppLifecycleState.detached:
// 앱이 종료되면 플레이어 리소스 해제
BGMAudioPlayer.dispose();
break;
}
}
처럼 각 화면 담당 State<>() 안에 두면 대기상태가 되면 잠시 음악을 멈추고, 앱이 종료되면 dispose()가 되게 할 수 있습니다. WidgetsBindingObserver를 쓰는 방법인데 State<> 선언 시그니처에
class _SongTitleGameScreenState extends State<SongTitleGameScreen> with WidgetsBindingObserver {
…
}
처럼 해두어야 쓸 수 있구요. initState()에
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
BGMAudioPlayer.resumeBackgroundMusic();
}
처럼 addObserver()를 시작해두어야 합니다.
이 WidgetsBindingObserver를 안쓰면 앱을 대기상태로 두어도 음악이 재생되기에 방해가 되니 꼭 추가해두어야 합니다.
- 구글 Gemini로 기능 질문을 하니 금방 해결되었습니다.