“Android – SoundPool use”

This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together

First, voice playback

Android development process more or less will come into contact with some sound playback, built-in components have MediaPlayer and SoundPool, MediaPlayer has its own shortcomings, resource occupancy, and can not support multiple audio playback at the same time.

SoundPool, on the other hand, is more suitable for short, rapid, and dense prompts. SoundPool supports asynchronous initialization and can be used to import and decode local audio files. This also comes with the disadvantage that if the resource is not fully loaded it will not play completely.

Second, the HandlerThread

It is important to understand HandlerThread before using SoundPool, using sound effects in games as an example. HandlerThread is a good choice because the main thread does not need to be used, but sound plays are more intensive. Why not common Thread? If you use regular threads, you need to wake up, wait, and notify the main Thread of updates. Cumbersome and difficult to manage.

1. What is HandlerThread?

As the name indicates, it is a Handler and also a Thread. It is familiar with the Handler mechanism, such as Thread switching, message processing, UI update, etc. The Thread class does nothing more than open child threads to handle time-consuming tasks. Take a look at the HandlerThread implementation source code.

  • Inherited from the Thread
public class HandlerThread extends Thread {
  int mPriority;
  int mTid = -1;
  Looper mLooper;
  private @Nullable Handler mHandler;
}
Copy the code

1. You can see that there are Looper and Handler instances inside

  • The constructor
public HandlerThread(String name) {
  super(name);
  mPriority = Process.THREAD_PRIORITY_DEFAULT;
}

public HandlerThread(String name, int priority) {
  super(name);
  mPriority = priority;
}
Process.THREAD_PRIORITY_DEFAULT = 0;Copy the code

1. The default priority of a thread is 0. In Java, the priority of a thread ranges from 1 to 10. In Android development, thread priority can be specified by Process, which can be viewed in the Process class. Of course, Google still recommends using the second method for prioritization. -20 indicates the highest priority.

  • The key method run()
@Override
public void run(a) {
  mTid = Process.myTid();
  Looper.prepare();
  synchronized (this) {
    mLooper = Looper.myLooper();
    notifyAll();
  }
  Process.setThreadPriority(mPriority);
  onLooperPrepared();
  Looper.loop();
  mTid = -1;
}
Copy the code

1. The implementation is very simple, just maintain a set of message loop, simple understanding is that classic interview question (how to create Handler in the child thread) concrete implementation.

The confusing part is the synchronization mechanism notifyAll(). If you think about it a little bit, the first thing you need to do is support the ability of the Handler to handle UI updates. The source code provides a method to obtain the current thread Handler instance object:

@NonNull
public Handler getThreadHandler(a) {
 if (mHandler == null) {
   mHandler = new Handler(getLooper());
 }
 return mHandler;
}
Copy the code

The Looper object must not be null in order for the mHandler object to be null as long as the thread is alive. Look at getLooper():

public Looper getLooper(a) {
  if(! isAlive()) {return null;
  }
  synchronized (this) {
     while (isAlive() && mLooper == null) {
          try {
           wait();
         } catch (InterruptedException e) {
       }
     }
  }
  return mLooper;
}
Copy the code

In the case of a thread already started, mLooper is initialized in the run() method:

public void run(a) {...synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); }... }Copy the code

If you don’t use wait-and-wake, there is no guarantee of synchronization when you call getThreadHandler externally, so it makes sense to use wait-and-wake to handle synchronization.

  • Safety exit
public boolean quitSafely(a) {
  Looper looper = getLooper();
  if(looper ! =null) {
     looper.quitSafely();
     return true;
  }
 return false;
}
Copy the code

The loop does not exit until all messages in the message queue have been processed. Note that the quitSafely also clears the queue of delayed messages and handles non-delayed messages. Quit clears all messages.

2. What can HandlerThread do?

The first thing to make clear is that this is a single-threaded class with Handler capabilities, so what does it do? Single thread + asynchronous operation is an obvious model to think of. For small file downloads, relatively simple I/O operations, simple database content reading and refreshing the UI, these scenarios are good. But it is not suitable for downloading large files, why? Since it is a single thread, downloading larger files is obviously not appropriate. Of course, there are other scenarios to be discussed here, with SoundPool for intensive voice broadcasting.

Third, SoundPool
1. Use the basic process

Take the broadcast collection amount as an example, the composition of the amount is very simple, nothing more than a number plus a decimal point plus a Chinese character “yuan”, and what we need to do is to decompose the target amount into a number (a single decimal point) plus “yuan”, through SoundPool loop playback of each syllable can meet the demand, The general flow of SoundPool is as follows:

  • Import the audio source file to the RES/RAW directory.
  • Customize SoundPool according to requirements and join voice queue;
  • Instantiate SoundPool and play it by play.
2. Basic build parameters
public SoundPool(int maxStreams, int streamType, int srcQuality) {
this(maxStreams,
  new AudioAttributes.Builder().setInternalLegacyStreamType(streamType).build());
        PlayerBase.deprecateStreamTypeForPlayback(streamType, "SoundPool"."SoundPool()");
}

private SoundPool(int maxStreams, AudioAttributes attributes) {
 super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_SOUNDPOOL);
 //do native setup
   if (native_setup(new WeakReference<SoundPool>(this), maxStreams, attributes) ! =0) {
    throw new RuntimeException("Native setup failed");
 }
 mAttributes = attributes;
 baseRegisterPlayer();
}

public static class Builder {
  private int mMaxStreams = 1;
  private AudioAttributes mAudioAttributes;
  / /...
}
Copy the code
  • MaxStreams: Maximum number of simultaneous audio playback supported.
  • StreamType: audio streamType, usually audiomanager.stream_music;
  • SrcQuality: the quality of the sampling conversion. Currently, it has no effect. The default value is 0.
  • Building SoundPool with Builder is recommended for versions 21 and above, the default constructor is obsolete.
3. Load the resource file
public int load(Context context, int resId, int priority) {
  AssetFileDescriptor afd = context.getResources().openRawResourceFd(resId);
  int id = 0;
  if(afd ! =null) {
      id = _load(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength(), priority);
      try {
         afd.close();
      } catch (java.io.IOException ex) {
        //Log.d(TAG, "close failed:", ex);}}return id;
}
Copy the code
  • Context: application context;
  • ResId: the ID of the resource, usually the file in res/raw directory, r.raw.test.
  • Priority: The priority of the audio file. It has no meaning. The default is 1 for compatibility and extension.
4. Configure playback
public final int play(int soundID, float leftVolume, float rightVolume,
  int priority, int loop, float rate) {
  baseStart();
  return _play(soundID, leftVolume, rightVolume, priority, loop, rate);
}
Copy the code
  • SoundID: ID of the resource returned in the load function;
  • LeftVolume: volume of the left channel, ranging from [0.0 to 1.0].
  • RightVolume: right channel volume, range [0.0-1.0];
  • Priority: indicates the priority of audio streams. 0 indicates the lowest priority.
  • Loop: indicates the loop mode of playback. 0 indicates the loop once. The number greater than 0 indicates the corresponding loop number.
  • Rate: indicates the playback speed. 1.0 indicates the normal playback speed. The value ranges from 0.5 to 2.0.

The usual way to set the volume is for the AudioManager to do a calculation:

AudioManager audioManager = (AudioManager) this.getSystemService(AUDIO_SERVICE);
// Get the maximum volume (current media type, STREAM_MUSIC used here)
int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
// Get the current volume (current media type, STREAM_MUSIC used here)
int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
// Sound volume ratio
float volumeRatio = (float) (currentVolume / maxVolume);
Copy the code
Four, SoundPool to achieve playback components

The form introduced by AAR is generally designed for the following reasons:

1. More general, can reuse components, easy to expand;

2. If there are too many local audio files, the package volume will be relatively large, which is not suitable for Apk volume optimization;

3. Load audio files on demand.

Now think about the requirements that should be included in the functionality of a simple SDK. First of all, there are different playback support. Some modules require a lot of resource files, while others require very few audio files. Load policy should be provided to download the corresponding audio files to the local as required. If there is audio that does not need to be spliced, ideally it can be played directly, then the universal play util is needed. For spliced audio, you need handlerThreads to handle continuous playback, but that’s an implementation detail. The other most important is the need for AIDL support (importing AAR files into local projects). The general idea has, then is the formulation of technical plan. Take the amount of play as an example, the main audio should contain numbers [0-9], decimal points., success and failure audio, etc.

  • Audio tools, mainly combing the corresponding relationship, for example, the number 0 corresponds to the string “0”, and the corresponding duration of the audio
object SourceUtil {
  // Numbers 0 to 9
  const val NUM_0 = "0"
  const val NUM_1 = "1"
  const val NUM_2 = "2"
  const val NUM_3 = "3"
  //....
  const val SUCCESS = "success"
  / /...
  
  // The duration of the audio playback
  val SOURCE_TO_DURATION = mapOf(
  	NUM_0 to 300L,
    NUM_1 to 300L./ /...
    SUCCESS to 1000L)}Copy the code
  • Load the corresponding audio resource policy interface
interface ILoadSource {
  fun loadSource(a): List<String>
}
Copy the code
  • Default load implementation, load all resources
class DefaultLoadAllSource : ILoadSource {
  override fun loadSource(a) : List<String> {
    return listOf(
      SourceUtil.NUM_0,
      SourceUtil.NUM_1,
      SourceUtil.NUM_2,
      //....
      SourceUtil.SUCCESS
    )
  }
}
Copy the code
  • Load resources as needed, such as only numbers
class LoadNumSource : ILoadSource {
  override fun loadSource(a) : List<String> {
    return listOf(
      SourceUtil.NUM_0,
      SourceUtil.NUM_1,
      SourceUtil.NUM_2,
      //....)}}Copy the code
  • Play the core class SoundPlayer

1. Load the specified file based on the resource alias.

2. Support to play single syllable in a loop (amount splicing);

3. Load resources and Unload Resources

class SoundPlayer {
  //SoundPool playback component
  private lateinit var mSoundPool : SoundPool
  // Play volume
  private var mVolume : Float? = 0f
  // Whether it is playing
  private var mPlaying = false
  // Id of the resource being played
  private var mCurrentSourceId = -1
  Alis and duration resource files loaded according to the loading policy
  private val mSourcePoolMap = mutableMapOf<String, SoundDTO>()
  // Play the queue
  private val mSourceQueue = LinkedList<String>
  // Loop through HandlerThread
  private val mHandler : Handler 
  	get() {
      val handlerThread = HandlerThread("coustom handlerThread name")
      // Start the thread
      handlerThread.start()
      return Handler(handlerThread.looper)
    }

  // Data DTO, which contains only the name alis, and the playback duration
  private data class SoundDTO(val sourceId: Int.val duration: Long)}Copy the code

The initialization operation must be compatible with the earlier version, and the audio resource file should be loaded locally according to the corresponding loading policy during initialization operation.

companion object {
  private const val AUDIO_PREFIX = "tts_"
  private const val MAX_STREAMS = 5
  private const val STREAM_TYPE = AudioManager.STREAM_MUSIC
}

fun initSoundPlayer(context: Context, iLoadSource: ILoadSource) {
  / / high version
  mSoundPool = if(Build.VERSION.SDK_INT >= 21) {
    SoundPool.Builder()
    				 .setMaxStreams(MAX_STREAMS)
    		     .setAudioAttributes(
             			AudioAttributes.Builder()
                                 .setLegacyStreamType(STREAM_TYPE)
                                 .build()
             )
    				.build()
  } else {
    SoundPool(MAX_STREAMS, STREAM_TYPE, 0)
  }
  mPlaying = false
  if (mSourcePoolMap.isNotEmpty()) {
     mSourcePoolMap.clear()
  }
  calculateVolume(context)
}

private fun calculateVolume(context: Context) {
 val audioManager: AudioManager? = (context.getSystemService(AUDIO_SERVICE) as   AudioManager?)
 valmaxVolume = audioManager? .getStreamMaxVolume(STREAM_TYPE)valcurrentVolume = audioManager? .getStreamVolume(STREAM_TYPE) mVolume =if(currentVolume ! =null) { maxVolume? .let { currentVolume.div(it).toFloat() } }else {
    0.8 f}}Copy the code

In order to play the audio in the queue one by one, we need to use Handler to tell the current playing result, that is, the current playing is over, and there are still files to play in the queue, so we continue to play the next audio.

fun playSource(sourceList: MutableList<String>) {
  sourceList.forEach {
    // Add a prefix and concatenate to the play column
    mSourceQueue.add("$AUDIO_PREFIX$it") } takeIf {! mPlaying}? .let { playNext() } }fun playNext(a){ takeIf {mSourceQueue.isNotEmpty()} ? .let {val sourceAlias = mSourceQueue.poll()
    valsoundDTO = sourceAlias? .let { mSourcePoolMap[sourceAlias] } soundDTO? .let { _soundDTO -> mCurrentSourceId = mSoundPool.play(_soundDTO.id, mVolume, mVolume,1.0.1.0 f)           
      mPlaying = true
      Duration Indicates the duration of the current audio
      mHandler.postDelayed(mNextRunnable, _soundDTO.duration)
    }? : run{
      mHandler.postDelayed(mNextRunnable, 0)}}? : run { stopPlay() } }private val mNextRunnable = Runnable {
  mSoundPool.stop(mCurrentSourceId)
  playNext()
}

private fun stopPlay(a) {
  mPlaying = false
  mSourceQueue.clear()
  mSoundPool.stop(mCurrentSourceId)
  mHandler.removeCallbacks(mNextRunnable)
}
Copy the code

For the loading of resources, through different loading strategies to compare alias and build SoundDTO objects stored in the map container, the specific implementation is relatively simple, not to paste the code, of course, also includes the definition of AIDL, basically the main implementation of the playback component on and off according to the specific business to achieve.

Five, the document

AudioManager

SoundPool

HandlerThread