본문 바로가기
Flutter/Package

Flutter와 just_audio로 구현하는 완벽한 갭리스(Gapless) 오디오 재생 가이드

by Maccrey Coding 2025. 4. 29.
반응형

 

안녕하세요, Flutter 개발자 여러분!

오늘은 많은 음악 앱, 명상 앱, 그리고 백색소음(white noise) 앱 개발자들이 겪는 공통적인 문제인 갭리스 오디오 재생(Gapless Audio Playback) 구현에 대해 알아보겠습니다.

특히 루프 재생 중 오디오 파일이 끝나고 다시 시작할 때 발생하는 그 짜증나는 '끊김 현상'을 완벽하게 해결하는 방법을 단계별로 소개해 드리겠습니다.

📌 목차

  1. 갭리스 오디오 재생이란?
  2. Flutter에서 갭리스 재생이 필요한 상황
  3. just_audio 패키지 소개
  4. 갭리스 재생을 위한 설정 방법
  5. 코드 실습: 끊김 없는 루프 재생 구현하기
  6. 오디오 파일 최적화 기법
  7. 고급 기법: 크로스페이드와 오디오 세션 관리
  8. 문제 해결과 FAQ

갭리스 오디오 재생이란?

갭리스 오디오 재생이란 한 마디로 오디오 파일 사이에 공백이나 끊김 없이 매끄럽게 재생하는 기술입니다.

음악 플레이어에서는 앨범의 트랙 간 자연스러운 전환을 위해, 명상 앱이나 백색소음 앱에서는 무한 루프 재생 시 끊김 없는 경험을 제공하기 위해 필수적인 기능입니다.

일반적인 오디오 재생 방식에서는 다음과 같은 문제가 발생합니다:

  1. 파일 간 전환 시 지연: 한 트랙이 끝나고 다음 트랙이 로드되는 시간 동안 짧은 무음 구간 발생
  2. 버퍼링 이슈: 오디오 파일을 충분히 미리 로드하지 않아 발생하는 끊김 현상
  3. 루프 재생 시 끊김: 같은 파일을 반복할 때 끝과 시작 부분에서 생기는 갭

이러한 문제들은 사용자 경험을 크게 저해하며, 특히 백색소음이나 명상 앱처럼 지속적인 소리가 중요한 애플리케이션에서는 치명적인 결함이 될 수 있습니다.


Flutter에서 갭리스 재생이 필요한 상황

Flutter 앱에서 갭리스 오디오 재생이 특히 필요한 상황은 다음과 같습니다:

  • 명상 앱: 지속적인 배경 음악이나 자연 소리를 끊김 없이 제공해야 함
  • 백색소음 앱: 수면, 집중, 명상을 위한 지속적인 소음 제공이 필수
  • 음악 플레이어: 라이브 앨범이나 디제이 믹스처럼 트랙 간 연속성이 필요한 경우
  • 게임 배경음악: 게임 내 배경 음악이 끊김 없이 반복되어야 하는 경우

이러한 앱들에서 갭이 발생하면 사용자의 몰입을 방해하고 앱의 품질을 낮게 평가받을 수 있습니다.


just_audio 패키지 소개

Flutter에서 갭리스 오디오 재생을 구현하기 위한 최적의 도구는 just_audio 패키지입니다. 이 패키지는 Ryan Heise가 개발한 강력한 오디오 플레이어 라이브러리로, 다음과 같은 장점을 제공합니다:

  1. 크로스 플랫폼 지원: iOS, Android, 웹 등 모든 Flutter 지원 플랫폼에서 동작
  2. 다양한 오디오 소스 지원: 로컬 파일, 원격 URL, 에셋 파일 등
  3. 고급 오디오 기능: 갭리스 재생, 루핑, 시크, 속도 조절 등
  4. 배터리 효율성: 네이티브 오디오 API를 사용하여 배터리 사용량 최적화
  5. 오디오 효과 및 이퀄라이저: 기본적인 오디오 처리 기능 내장

먼저 pubspec.yaml에 just_audio를 추가하는 것부터 시작해봅시다

dependencies:
  flutter:
    sdk: flutter
  just_audio: ^0.9.34
  audio_session: ^0.1.16  # 오디오 세션 관리를 위한 패키지

패키지를 설치한 후에는 다음 단계로 넘어가 갭리스 재생 설정 방법을 알아보겠습니다.


갭리스 재생을 위한 설정 방법

just_audio에서 갭리스 재생을 구현하기 위해서는 몇 가지 핵심 개념을 이해해야 합니다:

1. 오디오 소스의 종류

  • AudioSource.uri: 단일 오디오 파일을 위한 기본 소스
  • ConcatenatingAudioSource: 여러 오디오 소스를 이어붙이는 소스
  • LoopingAudioSource: 지정된 횟수만큼 반복 재생하는 소스
  • ClippingAudioSource: 오디오의 일부분만 재생하는 소스

2. 갭리스 재생을 위한 핵심 설정

갭리스 재생을 위해서는 다음 세 가지 핵심 요소가 필요합니다:

  1. 적절한 AudioSource 구성: LoopingAudioSource나 ConcatenatingAudioSource를 활용
  2. 사전 로딩(Preloading): 오디오 파일을 미리 메모리에 로드
  3. 올바른 오디오 세션 설정: 시스템의 오디오 세션을 갭리스 재생에 최적화

3. 플랫폼별 설정

Android와 iOS에서는 각각 다른 설정이 필요할 수 있습니다:

Android (AndroidManifest.xml):

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

iOS (Info.plist)에 추가:

<key>UIBackgroundModes</key>
<array>
    <string>audio</string>
</array>

코드 실습: 끊김 없는 루프 재생 구현하기

이제 실제로 갭리스 오디오 재생을 구현해보겠습니다. 먼저 기본적인 구현부터 시작해 점점 고도화해보겠습니다.

기본 구현: 단일 파일 갭리스 루프 재생

import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:audio_session/audio_session.dart';

class GaplessAudioPlayerScreen extends StatefulWidget {
  @override
  _GaplessAudioPlayerScreenState createState() => _GaplessAudioPlayerScreenState();
}

class _GaplessAudioPlayerScreenState extends State<GaplessAudioPlayerScreen> {
  late AudioPlayer _audioPlayer;
  bool _isPlaying = false;
  
  @override
  void initState() {
    super.initState();
    _audioPlayer = AudioPlayer();
    _initAudioPlayer();
  }
  
  @override
  void dispose() {
    _audioPlayer.dispose();
    super.dispose();
  }
  
  // 오디오 플레이어 초기화 및 갭리스 재생 설정
  Future<void> _initAudioPlayer() async {
    // 1. 오디오 세션 설정
    final session = await AudioSession.instance;
    await session.configure(const AudioSessionConfiguration(
      avAudioSessionCategory: AVAudioSessionCategory.playback,
      avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.mixWithOthers,
      avAudioSessionMode: AVAudioSessionMode.default_,
      androidAudioAttributes: AndroidAudioAttributes(
        contentType: AndroidAudioContentType.music,
        usage: AndroidAudioUsage.media,
      ),
    ));
    
    try {
      // 2. 갭리스 루프를 위한 오디오 소스 설정
      final loopingSource = LoopingAudioSource(
        // AudioPlayer.infiniteLoop는 무한 반복을 의미
        count: AudioPlayer.infiniteLoop,
        // 여기서 재생할 오디오 파일 지정
        child: AudioSource.uri(
          Uri.parse('asset:///assets/sounds/white_noise.mp3'),
          // MediaItem은 미디어 메타데이터를 지정하며 갭리스 재생을 위해 중요
          tag: MediaItem(
            id: '1',
            title: '백색소음',
          ),
        ),
      );
      
      // 3. 오디오 소스 설정 (preload=true로 사전 로딩 활성화)
      await _audioPlayer.setAudioSource(loopingSource, preload: true);
      
      // 4. 볼륨 및 재생 속도 설정
      _audioPlayer.setVolume(1.0);
      
      // 5. 상태 이벤트 리스너 등록
      _audioPlayer.playerStateStream.listen((state) {
        if (mounted) {
          setState(() {
            _isPlaying = state.playing;
          });
        }
      });
      
    } catch (e) {
      print('오디오 플레이어 초기화 오류: $e');
    }
  }
  
  // 재생/일시정지 토글 함수
  void _togglePlayback() {
    if (_isPlaying) {
      _audioPlayer.pause();
    } else {
      _audioPlayer.play();
    }
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('갭리스 오디오 플레이어')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              '끊김 없는 백색소음 재생',
              style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
            ),
            SizedBox(height: 30),
            IconButton(
              icon: Icon(_isPlaying ? Icons.pause_circle_filled : Icons.play_circle_filled),
              iconSize: 80,
              onPressed: _togglePlayback,
            ),
            SizedBox(height: 20),
            Text(
              _isPlaying ? '재생 중' : '일시정지됨',
              style: TextStyle(fontSize: 18),
            ),
          ],
        ),
      ),
    );
  }
}

코드 설명

위 코드를 한 줄씩 설명해보겠습니다:

  1. 필요한 패키지 가져오기:
    • Flutter 기본 UI를 위한 'material.dart'
    • 오디오 재생을 위한 'just_audio'
    • 오디오 세션 관리를 위한 'audio_session'
  2. StatefulWidget 정의:
    • 플레이어의 상태 변화를 반영하기 위한 StatefulWidget 사용
  3. AudioPlayer 인스턴스 생성:
    • AudioPlayer는 just_audio의 핵심 클래스로, 오디오 재생을 제어
  4. initState 메서드:
    • 위젯이 생성될 때 AudioPlayer 초기화
    • 오디오 플레이어 설정 함수 호출
  5. dispose 메서드:
    • 위젯이 제거될 때 AudioPlayer 자원 해제
  6. _initAudioPlayer 메서드:
    • 오디오 세션 구성: 백그라운드 재생 및 시스템 설정
    • LoopingAudioSource 설정: 무한 반복을 위한 소스 구성
    • preload: true 옵션으로 사전 로딩 활성화
    • 상태 변경 리스너 등록
  7. _togglePlayback 메서드:
    • 현재 재생 상태에 따라 재생/일시정지 토글
  8. build 메서드:
    • 재생 상태에 따른 UI 구성
    • 재생/일시정지 버튼과 현재 상태 표시

오디오 파일 최적화 기법

갭리스 재생을 위해서는 오디오 파일 자체도 최적화가 필요합니다. 특히 루프 재생에 최적화된 오디오 파일은 재생 품질을 크게 향상시킬 수 있습니다.

최적의 오디오 포맷

  1. MP3보다 OGG 또는 AAC 권장:
    • MP3는 파일 끝에 패딩(여백)이 추가되는 경우가 많아 갭리스 재생에 불리
    • OGG나 AAC는 갭리스 재생에 더 적합한 포맷
  2. 샘플링 레이트와 비트레이트:
    • 44.1kHz 샘플링 레이트, 256kbps 이상의 비트레이트 권장
    • 낮은 비트레이트는 파일 크기를 줄이지만 갭이 발생할 가능성 증가

FFmpeg를 이용한 오디오 최적화

Flutter에서는 flutter_ffmpeg 패키지를 사용해 오디오 파일을 최적화할 수 있습니다

import 'package:flutter_ffmpeg/flutter_ffmpeg.dart';

Future<String?> optimizeForGaplessPlayback(String inputPath) async {
  final FlutterFFmpeg flutterFFmpeg = FlutterFFmpeg();

  final directory = await getApplicationDocumentsDirectory();
  final outputPath = '${directory.path}/optimized_audio.ogg';
  
  // FFmpeg 명령어로 최적화 (OGG 포맷 변환 및 갭리스 최적화)
  int result = await flutterFFmpeg.execute(
    '-i "$inputPath" -c:a libopus -b:a 256k -af apad "$outputPath"'
  );
  
  if (result == 0) {
    return outputPath;
  } else {
    print('오디오 최적화 실패');
    return null;
  }
}

루프 포인트 최적화

백색소음처럼 자연스러운 루프가 필요한 경우, 시작과 끝부분을 크로스페이딩하여 부드러운 전환을 구현할 수 있습니다.

Future<String?> createLoopableWhiteNoise(int durationSeconds) async {
  final FlutterFFmpeg flutterFFmpeg = FlutterFFmpeg();
  final directory = await getApplicationDocumentsDirectory();
  final outputPath = '${directory.path}/loopable_noise.ogg';
  
  // 백색소음 생성 및 시작/끝 부분 페이드 처리
  int result = await flutterFFmpeg.execute(
    '-f lavfi -i anoisesrc=color=white:duration=$durationSeconds ' +
    '-af "afade=t=in:st=0:d=0.5,afade=t=out:st=${durationSeconds-0.5}:d=0.5" ' +
    '-c:a libopus -b:a 256k "$outputPath"'
  );
  
  if (result == 0) {
    return outputPath;
  } else {
    return null;
  }
}

고급 기법: 크로스페이드와 오디오 세션 관리

갭리스 재생을 더욱 완벽하게 만들기 위한 고급 기법들을 살펴보겠습니다.

크로스페이드 구현

여러 트랙 사이에서 부드러운 전환을 위한 크로스페이드 구현 방법입니다

Future<void> setupCrossfade() async {
  // 두 개의 오디오 플레이어 준비
  final player1 = AudioPlayer();
  final player2 = AudioPlayer();
  
  // 첫 번째 트랙 설정
  await player1.setAudioSource(
    AudioSource.uri(Uri.parse('asset:///assets/sounds/track1.mp3'))
  );
  
  // 두 번째 트랙 설정 (미리 로드)
  await player2.setAudioSource(
    AudioSource.uri(Uri.parse('asset:///assets/sounds/track2.mp3')),
    preload: true
  );
  
  // 첫 번째 트랙 재생
  await player1.play();
  
  // 첫 번째 트랙이 거의 끝나갈 때 두 번째 트랙 시작
  player1.positionStream.listen((position) {
    final duration = player1.duration;
    if (duration != null) {
      // 끝나기 3초 전에 페이드아웃 시작
      if (duration - position <= Duration(seconds: 3)) {
        // 볼륨 점진적 감소
        player1.setVolume(((duration - position).inMilliseconds / 3000));
        
        // 두 번째 트랙 시작 및 볼륨 점진적 증가
        if (!player2.playing) {
          player2.play();
          player2.setVolume(0);
          // 볼륨 점진적 증가
          Future.doWhile(() async {
            await Future.delayed(Duration(milliseconds: 100));
            final newVolume = player2.volume + 0.05;
            if (newVolume >= 1.0) {
              player2.setVolume(1.0);
              return false;
            }
            player2.setVolume(newVolume);
            return true;
          });
        }
      }
    }
  });
}

배터리 최적화 및 백그라운드 재생

장시간 재생에도 배터리 사용량을 최적화하는 설정입니다.

Future<void> setupOptimizedAudioSession() async {
  final session = await AudioSession.instance;
  await session.configure(AudioSessionConfiguration(
    // iOS 설정
    avAudioSessionCategory: AVAudioSessionCategory.playback,
    avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.mixWithOthers,
    avAudioSessionMode: AVAudioSessionMode.default_,
    
    // Android 설정 - 배터리 최적화
    androidAudioAttributes: AndroidAudioAttributes(
      contentType: AndroidAudioContentType.music,
      flags: AndroidAudioFlags.none,
      usage: AndroidAudioUsage.media,
    ),
    androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
    androidWillPauseWhenDucked: true,
  ));
  
  // 백그라운드 재생을 위한 설정
  await session.setActive(true);
}

문제 해결과 FAQ

자주 발생하는 문제와 해결책

Q: 안드로이드에서 갭리스 재생이 잘 작동하지 않아요.

A: 안드로이드에서는 기기별로 오디오 시스템 차이가 있을 수 있습니다. AndroidManifest.xml에 필요한 권한이 모두 추가되었는지 확인하고, 오디오 세션 설정을 정확히 해주세요. 또한 오디오 포맷을 OGG나 AAC로 변경해보세요.

Q: 앱이 백그라운드로 가면 오디오가 멈춰요.

A: iOS의 경우 Info.plist에 백그라운드 오디오 모드를 추가했는지 확인하세요. 또한 AudioSession 설정에서 avAudioSessionCategory를 playback으로 지정했는지 확인하세요.

Q: 루프 재생 시 가끔 약간의 끊김이 있어요.

A: 오디오 파일 자체의 최적화가 필요할 수 있습니다. FFmpeg를 사용해 파일 끝의 패딩을 제거하거나, 크로스페이드를 적용해보세요. 또한 preload: true 옵션을 반드시 설정했는지 확인하세요.

Q: 메모리 사용량이 너무 많아요.

A: 고품질 오디오 파일을 preload하면 메모리 사용량이 증가할 수 있습니다. 메모리 제약이 심한 환경에서는 파일 크기를 줄이거나, 긴 파일의 경우 필요한 부분만 ConcatenatingAudioSource로 분할하여 로드하는 방법을 고려해보세요.


결론

갭리스 오디오 재생은 고품질 오디오 앱을 위한 필수 기능입니다. Flutter와 just_audio 패키지를 이용하면 iOS와 Android 모두에서 원활한 갭리스 재생을 구현할 수 있습니다. 핵심은 다음과 같습니다:

  1. 올바른 오디오 소스 설정: LoopingAudioSource나 ConcatenatingAudioSource 활용
  2. 사전 로딩: preload: true 옵션으로 오디오 미리 로드
  3. 오디오 세션 최적화: 배터리 사용량과 백그라운드 재생을 고려한 설정
  4. 오디오 파일 최적화: 고품질 포맷 사용 및 파일 최적화

이러한 기법들을 적용하면 음악 앱, 명상 앱, 백색소음 앱 등에서 사용자에게 끊김 없는 완벽한 오디오 경험을 제공할 수 있습니다.

째깍째깍...흘러가는 시간 붙잡고 싶다면? 

Study Duck 학습 타이머 즉시 ON! 랭킹 경쟁 참여하고 학습 습관 만들 기회, 놓치지 마세요!

www.studyduck.net

반응형