just_audio로 배경음악 연주하는 코드들 해설

Posted on

플러터에서 배경음악 연주를 할때 중요한 것은 지연시간입니다.

음악 연주를 하다가 화면이 바뀔때 다른 배경음악을 연주해야 한다면 아래와 같은 현상이 있게 됩니다.

  • 재생이 끝나고 다음 재생을 시작할때까지의 지연시간이 생각보다 긴 경우가 있게 됨

이 지연시간을 줄인 패키지가 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로 기능 질문을 하니 금방 해결되었습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다