Android SoundPool: get notified when end of played-ThrowExceptions

Exception or error:

This sound so simple that I can’t figure out why I can’t find the answer lol

I have a working sound pool class (thanks to a tutorial and some tweaking I did), and it works fine.

the problem now is that I want to be able to change my background music randomly. (not always have the same music in a loop but have 2 or 3 and when one finishes I play one of the 2 others).

problem is I can’t find a way to get notified that the music has finished playing.

Any ideas ?

Jason

How to solve:

It can’t be done with SoundPool as far as I can tell.

The only audio ‘player’ that I know which can provide a completion notification is
MediaPlayer – it’s more of a complex beast than SoundPool but allows setting an OnCompletionListener to be notified when playback is complete.

###

This is what I do:

On startup I get the length of each sound-click using a MediaPlayer:

private long getSoundDuration(int rawId){
   MediaPlayer player = MediaPlayer.create(context, rawId);
   int duration = player.getDuration();
   return duration;
}

and store the sound plus the duration together (in a DTO-type object).

###

I have more than 100 short sound clips and SoundPool is my best option. I want to play one clip just after another clip is finished playing. Upon finding that there is no onCompletionListener() equivalent I chose to implement a runnable. This works for me because the first sound is between 1 and 2 seconds long so I have the duration of the runnable set at 2000. Hope they work on this class because its got lots of potential!

###

MediaPlayer is heavy and slow compared with SoundPool, but SoundPool doesn’t have setOnCompletionListener. To cope with this issue I implemented a custom class from SoundPool with setOnCompletionListener.

usage: similar to MediaPlayer

creation:

SoundPoolPlayer mPlayer = SoundPoolPlayer.create(context, resId);
mPlayer.setOnCompletionListener(
    new MediaPlayer.OnCompletionListener() {
        @Override
        public void onCompletion(MediaPlayer mp) {  //mp will be null here
            Log.d("debug", "completed");
        }
    };
);
mPlayer.play();

pause:

mPlayer.pause();

stop:

mPlayer.stop();

resume:

mPlayer.resume();

isPlaying:

mPlayer.isPlaying();

any pull request is welcomed. I only implemented what I need here.

###

I was facing a similar problem and created s SoundPool queue which enqueue the sounds to be played and notifies when each sound completes it’s playback. It’s in Kotlin but should be easy translated to Java.

class SoundPoolQueue(private val context: Context, maxStreams: Int) {
    companion object {
        private const val LOG_TAG = "SoundPoolQueue"
        private const val SOUND_POOL_HANDLER_THREAD_NAME = "SoundPoolQueueThread"
        private const val ACTION_PLAY_SOUND = 1

        @JvmStatic
        fun getSoundDuration(context: Context, soundResId: Int) : Long {
            val assetsFileDescriptor = context.resources.openRawResourceFd(soundResId)
            val mediaMetadataRetriever = MediaMetadataRetriever()

            mediaMetadataRetriever.setDataSource(
                    assetsFileDescriptor.fileDescriptor,
                    assetsFileDescriptor.startOffset,
                    assetsFileDescriptor.length)

            val durationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
            val duration = durationString.toLong()

            logDebug("SoundPoolQueue::getSoundDuration(): Sound duration millis: $durationString")

            assetsFileDescriptor.close()
            return duration
        }

        @JvmStatic
        private fun logDebug(message: String) {
            if(!BuildConfig.DEBUG) {
                return
            }

            Log.d(LOG_TAG, message)
        }
    }

    private var soundPool = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        val attrs = AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT)
                .setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
                .build()

        SoundPool.Builder()
                .setMaxStreams(maxStreams)
                .setAudioAttributes(attrs)
                .build()
    }
    else {
        @Suppress("DEPRECATION")
        SoundPool(maxStreams, AudioManager.STREAM_NOTIFICATION, 0)
    }

    var soundPoolQueueListener : SoundPoolQueueListener? = null

    private val soundPoolHandlerThread = SoundPoolQueueThread().apply { start() }
    private val soundPoolSoundsSparseArray = SparseArray<SoundPoolSound>()
    private val soundPoolSoundsQueue = LinkedList<SoundPoolSound>()

    fun addSound(soundResId: Int, leftVolume: Float, rightVolume: Float, priority: Int, loop: Boolean, rate: Float) {
        val durationMillis = getSoundDuration(context = context, soundResId = soundResId)
        val soundId = soundPool.load(context, soundResId, priority)

        soundPoolSoundsSparseArray.put(soundResId,
                SoundPoolSound(durationMillis, soundResId, soundId, leftVolume, rightVolume, priority, loop, rate))
    }

    fun playSound(soundResId: Int) {
        logDebug("SoundPoolQueue::playSound()")
        soundPoolSoundsQueue.add(soundPoolSoundsSparseArray[soundResId])
        soundPoolHandlerThread.handler?.sendEmptyMessage(ACTION_PLAY_SOUND)
    }

    inner class SoundPoolQueueThread : HandlerThread(SOUND_POOL_HANDLER_THREAD_NAME) {
        var handler: Handler? = null

        override fun onLooperPrepared() {
            super.onLooperPrepared()

            handler = object : Handler(looper) {
                override fun handleMessage(msg: Message) {
                    super.handleMessage(msg)

                    if(msg.what == ACTION_PLAY_SOUND && handler!!.hasMessages(ACTION_PLAY_SOUND)) {
                        return
                    }

                    if(soundPoolSoundsQueue.isEmpty()) {
                        logDebug("SoundPoolHandlerThread: queue is empty.")
                        handler!!.removeMessages(ACTION_PLAY_SOUND)
                        return
                    }

                    logDebug("SoundPoolHandlerThread: Playing sound!")
                    logDebug("SoundPoolHandlerThread: ${soundPoolSoundsQueue.size} sounds left for playing.")

                    val soundPoolSound = soundPoolSoundsQueue.pop()
                    soundPool.play(soundPoolSound.soundPoolSoundId,
                            soundPoolSound.leftVolume,
                            soundPoolSound.rightVolume,
                            soundPoolSound.priority,
                            if(soundPoolSound.loop) { 1 } else { 0 },
                            soundPoolSound.rate)

                    try {
                        Thread.sleep(soundPoolSound.duration)
                    }
                    catch (ex: InterruptedException) { }

                    //soundPoolQueueListener?.onSoundPlaybackCompleted(soundPoolSound.soundResId)
                    sendEmptyMessage(0)
                }
            }
        }
    }

    interface SoundPoolQueueListener {
        fun onSoundPlaybackCompleted(soundResId: Int)
    }
}

The accompanying data class

data class SoundPoolSound(val duration: Long,
                      val soundResId: Int,
                      val soundPoolSoundId: Int,
                      val leftVolume: Float,
                      val rightVolume: Float,
                      val priority: Int,
                      val loop: Boolean,
                      val rate: Float)

You will get notified when sound has completed playing in the

onSoundPlaybackCompleted(soundResId: Int)

with the resouce id of the completed playback sound.

Usage example:

 private class SoundPoolRunnable implements Runnable {
        @Override
        public void run() {
            LogUtils.debug(SerializableNames.LOG_TAG, "SoundPoolRunnable:run(): Initializing sounds!");

            m_soundPoolPlayer = new SoundPoolQueue(GSMSignalMonitorApp.this, 1);
            m_soundPoolPlayer.setSoundPoolQueueListener(new SoundPoolQueue.SoundPoolQueueListener() {
                @Override
                public void onSoundPlaybackCompleted(int soundResId)
                {
                    LogUtils.debug(SerializableNames.LOG_TAG, "onSoundPlaybackCompleted() " + soundResId);
                }
            });

            m_soundPoolPlayer.addSound(R.raw.gsm_signal_lost, 0.2f, 0.2f, 1, false, 1.0f);
            m_soundPoolPlayer.addSound(R.raw.gsm_signal_restored, 0.2f, 0.2f, 1, false, 1.0f);
            m_soundPoolPlayer.addSound(R.raw.gsm_signal_low, 0.2f, 0.2f, 1, false, 1.0f);

            m_soundPoolPlayer.addSound(R.raw.gsm_signal_lost_ru, 0.2f, 0.2f, 1, false, 1.0f);
            m_soundPoolPlayer.addSound(R.raw.gsm_signal_restored_ru, 0.2f, 0.2f, 1, false, 1.0f);
            m_soundPoolPlayer.addSound(R.raw.gsm_signal_low_ru, 0.2f, 0.2f, 1, false, 1.0f);
        }
    }

Hope it helps 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *