본문 바로가기

Android

SoundPool을 이용한 안드로이드의 효과음 재생 예제

안드로이드에서 오디오 파일을 재생하기 위한 방법은 크게 2가지가 있는데, 하나는 MediaPlayer를 사용하는 것이고 다른 하나는 여기서 살펴볼 SoundPool을 이용하는 것입니다.


둘의 차이는 MediaPlayer는 상대적으로 음악과 같이 음원의 길이가 긴 것들을 한 번에 하나씩 재생하는데 용이한 구조이고, SoundPool은 반대로 게임의 효과음처럼 짧으면서 여러개를 동시에 재생할 필요가 있는 것들을 다룰 때 유용합니다.



1. 음원의 종류 및 위치


지원하는 종류는 https://developer.android.com/guide/topics/media/media-formats.html 에 나와 있듯이 거의 대부분의 음원이라고 생각하면 될 것 같습니다.


음원은 res/raw 아래 위치하면 됩니다. (디렉토리로 보면 app\src\main\res\raw)



2. SoundPool 생성


LolliPop 이전에는 단순히 생성자로 생성하던 것을 LolliPop 이후에는 Builder()를 가지고 생성하도록 되어 있습니다. 그래서 기기의 버전에 따라 분기해서 생성하도록 하면 될 것 같습니다.

AudioAttributes.Builder의 setUsage()나 setContentType()의 값은 https://developer.android.com/reference/android/media/AudioAttributes.Builder.html 를 참조하면 되나 현재로써는 값에 따라 그리 크게 다른 점은 없어 보입니다.


LolliPop 이후의 setMaxStream()과 이전 버전의 SoundPool의 첫번째 인수는 동시에 재생할 수 있는 개수를 말하는데 이 샘플 예제에서는 계이름 개수에 맞춰 8개로 설정했습니다. 이 값을 1로 줄이고 건반(?)들을 동시에 눌러보면 차이점을 쉽게 알 수 있습니다. 


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build();
            soundPool = new SoundPool.Builder().setAudioAttributes(audioAttributes).setMaxStreams(8).build();
        }
        else {
            soundPool = new SoundPool(8, AudioManager.STREAM_NOTIFICATION, 0);
        }



3. 음원 load


MediaPlayer를 보면 음원을 재생할 준비를 하는 prepare()와 prepareAsync() 라는 2개의 메소드가 있습니다. 차이점은 이름에서 알 수 있듯이 prepare()는 synchronous 방식으로 음원을 재생할 준비를 하는 동안 다른 작업을 할 수 없습니다. 반면에 prepareAsync()는 asynchronous 방식으로 다른 작업을 병행하면서 준비가 완료되면 MediaPlayer의 setOnPreparedListener() 메소드를 통해 등록한 MediaPlayer.OnPreparedListener의 onPrepared() 메소드를 호출하여 재생할 준비가 되었음을 알려주게 됩니다.


반면에 SoundPool은 prepare()나 prepareAsync()와 같은 메소드가 없고 load() 메소드를 통해 음원을 로딩하게 됩니다. 문제는 load() 메소드가 완료된 후 바로 play()로 재생을 하는 경우 제대로 안되는 경우가 많습니다. 좀 더 살펴보면 SoundPool도 MediaPlayer와 비슷하게 SoundPool의 setOnLoadCompleteListener()를 통해 SoundPool.OnLoadCompleteListener를 등록하면, 음원의 loading 완료 후 onLoadComplete() 메소드가 호출되게 됩니다. 이를 보면 SoundPool의 load() 메소드는 Asynchronous하다는 것을 알 수 있습니다.


하지만 경험상 약 100여개의 짧은 소리들을 load() 메소드로 로딩했을 때 좀 오래된 기기에서 10여초 정도 소요되는 것을 확인했습니다. (onLoadComplete() 메소드가 호출되는 데 10초가 아니라 load() 메소드를 100여번 호출하는데 걸린 시간입니다.) 사용자에게 10초를 기다리라고 하는 것은 그리 좋은 방법이 아니니 별도의 Thread를 사용한다던지 하는 별도의 방법을 고려해야 할 것 같습니다. (하나를 로딩하는 데는 0.1초 정도 걸리는 것이기 때문에 ANR(Application Not Responding)의 문제는 없을 듯 합니다.)


여기서는 클릭 이벤트가 발생했을 때 필요한 음원이 이미 로딩되어 있으며 바로 재생을 하고, 그렇지 않으면 음원을 로딩하고 완료되면 해당 음원을 재생하는 식으로 처리합니다.


SoundPool의 load() 메소드는 반환값이 양수입니다. 때문에 초기화를 0으로 해놓고 0이면 load()하고 아니면 play()를 호출하도록 합니다.


1
2
3
4
5
6
            if (soundIds[soundIndex] == 0) {
                // ex, rawSound == R.raw.mi (미)
                soundIds[soundIndex] = soundPool.load(MainActivity.this, rawSound, 1);
            } else {
                soundPool.play(soundIds[soundIndex], 1f, 1f, 0, 0, 1f);
            }


onLoadComplete()에서는 완료되면 그 음원을 play() 합니다. 이 예제에서는 무조건 로딩을 끝나면 재생을 하는 식으로 했으나, 처음에 전부 로딩을 하고 재생을 하는 식으로 구현한다면 이 부분의 로직이 변경되어야 합니다.


1
2
3
4
5
6
        soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
            @Override
            public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
                soundPool.play(sampleId, 1f, 1f, 0, 0, 1f);
            }
        });



4. 음원 재생


위에서도 본 바와 같이 SoundPool의 음원 재생은 play() 메소드로 합니다. 이 메소드의 6개 파라미터의 의미는 다음과 같습니다.


 파라미터

타입 

의미 

 soundID

 int

 load() 메소드로 얻은 음원의 ID

 leftVolume

 float

 왼쪽 소리 크기 (0.0 ~ 1.0)

 rightVolume

 float

 오른쪽 소리 크기 (0.0 ~ 1.0)

 priority

 int

 우선 순위 (0이 가장 낮음)

 loop

 int

 반복 여부 (0 : 1번 재생, -1 : 무한 반복)

 rate

 float

 재생 속도 (1.0 : 정상 속도, 0.5 ~ 2.0 (2배속))



5. SoundPool의 초기화 및 해제 위치


또 안드로이드 개발자 싸이트에 보면 생성은 onStart() 메소드에서 하고, onStop() 메소드에서 release() 하라고 말하고 있습니다. 그러니 만약 다른 코드가 없다면 onStart()에서 SoundPool을 생성하고 onStop()에서 해제 하는 것은 다음과 같습니다.


1) 초기화


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    @Override
    protected void onStart() {
        super.onStart();

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            AudioAttributes audioAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_NOTIFICATION)
                    .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
                    .build();
            soundPool = new SoundPool.Builder().setAudioAttributes(audioAttributes).setMaxStreams(8).build();
        }
        else {
            soundPool = new SoundPool(8, AudioManager.STREAM_NOTIFICATION, 0);
        }

        soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
            @Override
            public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
                soundPool.play(sampleId, 1f, 1f, 0, 0, 1f);
            }
        });
    }


2) 해제


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
    @Override
    protected void onStop() {
        super.onStop();

        for (int i = 0 ; i < soundIds.length ; i++) {
            soundIds[i] = 0;
        }

        soundPool.release();
        soundPool = null;
    }



※ 이 프로젝트는 https://github.com/zeany/doremi 에 있으며 아래 명령으로 소스를 가져올 수 있습니다.


git clone https://github.com/zeany/doremi.git DoReMi