5

I was following a tutorial to integrate SoundPool into my app, and this is the code which was given in the tutorial:

package com.example.soundpoolexample;

import android.app.Activity;
import android.media.AudioManager;
import android.media.SoundPool;
import android.media.SoundPool.OnLoadCompleteListener;
import android.os.Bundle;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;

public class SoundPoolExample extends Activity implements OnTouchListener {
    private SoundPool soundPool;
    private int soundID;
    boolean loaded = false;

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        View view = findViewById(R.id.textView1);
        view.setOnTouchListener(this);
        // Set the hardware buttons to control the music
        this.setVolumeControlStream(AudioManager.STREAM_MUSIC);
        // Load the sound
        soundPool = new SoundPool(10, AudioManager.STREAM_MUSIC, 0);
        soundPool.setOnLoadCompleteListener(new OnLoadCompleteListener() {
            @Override
            public void onLoadComplete(SoundPool soundPool, int sampleId,
                    int status) {
                loaded = true;
            }
        });
        soundID = soundPool.load(this, R.raw.sound1, 1);

    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            // Getting the user sound settings
            AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
            float actualVolume = (float) audioManager
                    .getStreamVolume(AudioManager.STREAM_MUSIC);
            float maxVolume = (float) audioManager
                    .getStreamMaxVolume(AudioManager.STREAM_MUSIC);
            float volume = actualVolume / maxVolume;
            // Is the sound loaded already?
            if (loaded) {
                soundPool.play(soundID, volume, volume, 1, 0, 1f);
                Log.e("Test", "Played sound");
            }
        }
        return false;
    }
}

The problem is, I want to make sure that the application only moves forward once the SoundPool is loaded successfully, and not have to check if it is loaded before playing a sound each time. How would I do this?

I am sure this is an atrocious solution, but would something like this work after I load the sound file in onCreate?:

...

    soundID = soundPool.load(this, R.raw.sound1, 1);
    while (!loaded) {}

...

I am sure there is a better way.

capcom
  • 3,257
  • 12
  • 40
  • 50

4 Answers4

1

You can have an elegant solution using Semaphore, however try to avoid blocking onCreate(), since the app will not be responsive, the code here is just to illustrate the use of Semaphore

public class SoundPoolExample extends Activity implements OnTouchListener {

public static final Semaphore semaphore = new Semaphore(0);

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    soundPool.setOnLoadCompleteListener(new OnLoadCompleteListener() {
        @Override
        public void onLoadComplete(SoundPool soundPool, int sampleId,
                int status) {

            semaphore.release();
        }
    });
    soundID = soundPool.load(this, R.raw.sound1, 1);

    // Will block since there is not permits (the semaphore is initialized with 0)
    // then will continue once semaphore.release(); is called i.e. after loading is finished
    semaphore.acquire();
} 

// rest of your class

}
iTech
  • 18,192
  • 4
  • 57
  • 80
  • Thanks, looking into it. Is this the way all other apps wait for their resources to load? I mean, when you open any app, there is a loading screen. So are they running Semaphore in the background, or something else? – capcom Feb 08 '13 at 22:56
  • Definitely some apps are doing it with `Semaphore`, you can also do this with `wait/notify`, which is helpful if you want to wait up to a maximum amount of time. – iTech Feb 08 '13 at 22:59
  • When using several sounds with the SoundPool (which it is designed for), I believe a CountDownLatch would be a better match than the Semaphore. – personne3000 Jul 13 '14 at 07:30
1

I think while using SoundPool, everyone should pay attention to two cases:

  1. SoundPool is deprecated for API+21, instead you should use SoundPool.Builder
  2. If your SoundPool runs in the main thread, take it to another thread as it blocks your UI
MBaas
  • 7,248
  • 6
  • 44
  • 61
sam
  • 31
  • 3
0

I tried iTech's semaphore solution and it didn't work. The application sits there waiting forever.

onCreate is called with the main thread. This thread creates the sound pool, starts the loading of the sound, and then parks itself waiting for the loading to complete. Seems like it should work, right?

After some investigation, I found that SoundPool has the undocumented behavior of always running onLoadComplete with the main thread using the Looper/Handler mechanism. Since the main thread is parked, it cannot receive that event, and the application hangs.

We can see how this works in the Android source code in SoundPool.java:

public void setOnLoadCompleteListener(SoundPool.OnLoadCompleteListener listener)
{
    synchronized(mLock) {
        if (listener != null) {
            // setup message handler
            Looper looper;
            if ((looper = Looper.myLooper()) != null) {
                mEventHandler = new EventHandler(mProxy, looper);
            } else if ((looper = Looper.getMainLooper()) != null) {
                mEventHandler = new EventHandler(mProxy, looper);
            } else {
                mEventHandler = null;
            }
        } else {
            mEventHandler = null;
        }
        mOnLoadCompleteListener = listener;
    }
}

This shows that it uses the main thread looper by default unless you have a looper of your own set up.

The simplest solution is to run the entire sound loading procedure in a new thread and return from the main thread immediately. You can then use the main thread to show a loading screen if necessary. When the sound is loaded, the main thread is free to receive the onLoadComplete call and signal the sound loading thread. The sound loading thread can wait for that signal with a semaphore or wait/notify on a shared Object.

ajw
  • 499
  • 6
  • 6
0

This is a bit tricky to solve correctly. I handle similar scenario in the following way. Structure-wise, it looks like this:

  1. handler is instantiated on the main thread.
  2. handleMessage is implemented, which takes care of a custom notification message of all sound samples being loaded.
  3. A SoundPool is instantiated on the main thread.
  4. New thread is started which:

    4.1 Sets up SoundPool.OnLoadCompleteListener which sends the custom notification message using the instantiated handler from the main thread.

    4.2 Uses instantiated soundPool to issue async loading of sound samples (load method of SoundPool class)

Then, there is a variable which holds a state of sound samples loading. It can have three states: not loading, loading, loaded.

The state of this variable is set to loading in the thread from the 4th step above.

The state of this variable is set to loaded in the handleMessage when the custom notification message arrives.

This state variable is checked before any sound is played. If no sound samples are loaded yet, a semi-fullscreen dialog is shown stating that the sound samples are being loaded and automatically disappears when the loading is done. The dialog can't be dismissed by a user. That is does programmatically from handleMessage method by calling the dismiss method of that dialog, if shown. The dialog has a nice progress bar animation. This is specific to my app and it can be done differently for other apps.

You might ask why it is done so complicated. The thing is that the whole Android GUI API is an event machine and by definition, your own code running on the main thread should not execute any time consuming procedure because that would render the whole GUI non-responsive. In fact, when Android detects such cases, it logs them into logcat. For example, if I had used a semaphore, there would be no progress bar animation because the main GUI thread would be blocked. Using a semaphore this way just exhibits the fact of not understanding Android GUI architecture and it can easily cause an app to hang if something goes wrong with a part which releases the semaphore.

Hope this helps.

Peter Vrabel
  • 372
  • 1
  • 8