Android SoundPool 的使用

/ Android / 没有评论 / 1666浏览

Android SoundPool 的使用

概述

最近工作接触到Android 中的 SoundPool 类,使用后发觉很是有意思,记录一下以备后查。

Android 开发中,难免会碰到音视频相关的处理。SoundPool 是 Android 提供的一个API类,用来播放简短的音频,使用简单但功能相对强大。只需花很少的气力,就可以完成音频的播放、暂停、恢复及停止等操作。从名字上也可以看出,它是一个“pool”,我们可以加载多个音频资源到内存,进行管理与播放,比如控制同时播放流的最大数目。加载资源到内存是需要花费少许时间的,因此我们需要监听加载资源完毕的事件,在加载完毕后才能进行播放,以免发生不可预期的错误。

除了以上介绍外,SoundPool 还有诸多其他功能,诸如调节左右声道的音量值、调整播放的语速、设置播放的优先级以及播放的次数等等,说起来还是挺有意思的。接下来述说 SoundPool 的具体使用。

SoundPool 的创建

使用 SoundPool 首先需创建它。SoundPool 的创建方式在不同版本中会有所不同,为了更好的兼容性,应该对api版本进行判断,再对应的进行创建. 在 5.0 以前 ,直接使用它的构造方法即可,而在这之后,则需要使用Builder模式来创建。

创建完 SoundPool 后,通过setOnLoadCompleteListener设置监听,用来监听资源加载完毕的事件发生。这主要是为了播放做准备.通过名字可猜测到, 当音频资源加载完成后,会回调设置的监听的onLoadComplete方法, 在这个方法里, 可以进行播放音频

/**
 * 创建SoundPool ,注意 api 等级
 */
private void createSoundPoolIfNeeded() {
    if (mSoundPool == null) {
        // 5.0 及 之后
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            AudioAttributes audioAttributes = null;
            audioAttributes = new AudioAttributes.Builder()
                .setUsage(AudioAttributes.USAGE_MEDIA)
                .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
                .build();

            mSoundPool = new SoundPool.Builder()
                .setMaxStreams(16)
                .setAudioAttributes(audioAttributes)
                .build();
        } else { 
            // 5.0 以前
            // 创建SoundPool
            mSoundPool = new SoundPool(16, AudioManager.STREAM_MUSIC, 0); 
        }
        // 设置加载完成监听
        mSoundPool.setOnLoadCompleteListener(this); 
    }
}	

5.0 之前,直接通过SoundPool的构造方法来创建,有三个参数:

  1. maxStreams 同时播放流的最大数量,当播放的流的数目大于此值,则会选择性停止优先级较低的流
  2. streamType 流类型,比如 STREAM_MUSIC
  3. srcQuality 采样率转换器质量,目前没有什么作用,默认填充0

如下是方法原型,注释做了详细的说明:

/**
 * Constructor. Constructs a SoundPool object with the following
 * characteristics:
 *
 * @param maxStreams the maximum number of simultaneous streams for this
 *                   SoundPool object
 * @param streamType the audio stream type as described in AudioManager
 *                   For example, game applications will normally use
 *                   {@link AudioManager#STREAM_MUSIC}.
 * @param srcQuality the sample-rate converter quality. Currently has no
 *                   effect. Use 0 for the default.
 * @return a SoundPool object, or null if creation failed
 * @deprecated use {@link SoundPool.Builder} instead to create and configure a
 *     SoundPool instance
 */
public SoundPool(int maxStreams, int streamType, int srcQuality) {
    // ...
}

5.0 之后,使用Builder模式进行构造.Builder可以设置多个参数.

如下代码摘录自 SoundPool.Builder 类,可以发现给 setAudioAttributes 传 null 是会抛出异常的,如果不设置的话,在build方法中则会默认创建一个,usage设置为USAGE_MEDIA

public Builder setAudioAttributes(AudioAttributes attributes)
    throws IllegalArgumentException {
    if (attributes == null) {
        throw new IllegalArgumentException("Invalid null AudioAttributes");
    }
    mAudioAttributes = attributes;
    return this;
}

public SoundPool build() {
    if (mAudioAttributes == null) {
        mAudioAttributes = new AudioAttributes.Builder()
            .setUsage(AudioAttributes.USAGE_MEDIA).build();
    }
    return new SoundPool(mMaxStreams, mAudioAttributes);
}

AudioAttributes 也是使用Builder模式来构造的,setUsage用来设置用途(比如是游戏还是媒体), setContentType用来设置内容类型(比如是视频还是音乐)

通过setUsage方法可知它支持的usage类型,还是挺多的:

/**
	 * Sets the attribute describing what is the intended use of the the audio signal,
	 * such as alarm or ringtone. ...
	 */
public Builder setUsage(@AttributeUsage int usage) {
    switch (usage) {
        case USAGE_UNKNOWN:
        case USAGE_MEDIA:
        case USAGE_VOICE_COMMUNICATION:
        case USAGE_VOICE_COMMUNICATION_SIGNALLING:
        case USAGE_ALARM:
        case USAGE_NOTIFICATION:
        case USAGE_NOTIFICATION_RINGTONE:
        case USAGE_NOTIFICATION_COMMUNICATION_REQUEST:
        case USAGE_NOTIFICATION_COMMUNICATION_INSTANT:
        case USAGE_NOTIFICATION_COMMUNICATION_DELAYED:
        case USAGE_NOTIFICATION_EVENT:
        case USAGE_ASSISTANCE_ACCESSIBILITY:
        case USAGE_ASSISTANCE_NAVIGATION_GUIDANCE:
        case USAGE_ASSISTANCE_SONIFICATION:
        case USAGE_GAME:
        case USAGE_VIRTUAL_SOURCE:
        case USAGE_ASSISTANT:
            mUsage = usage;
            break;
        default:
            mUsage = USAGE_UNKNOWN;
    }
    return this;
}

通过setContentType方法可知支持的contentType:

/**
 * Sets the attribute describing the content type of the audio signal, such as speech,or music.
 */
public Builder setContentType(@AttributeContentType int contentType) {
    switch (contentType) {
        case CONTENT_TYPE_UNKNOWN:
        case CONTENT_TYPE_MOVIE:
        case CONTENT_TYPE_MUSIC:
        case CONTENT_TYPE_SONIFICATION:
        case CONTENT_TYPE_SPEECH:
            mContentType = contentType;
            break;
        default:
            mUsage = CONTENT_TYPE_UNKNOWN;
    }
    return this;
}

资源加载与播放

播放不是一蹴而就的,如前所述,需要在资源加载完毕后,才能调用play方法进行播放。因此,直接调用play方法是不可取的,而且可能造成难以预料的错误。

因此我把这个方法取为 playMayWait, 当资源没加载时,首先需要调用 SoundPool 的load 方法进行加载,并返回一个mSoundId,它代表一个已加载的资源的id,可以用来播放音效。

加载音频资源异步执行,此过程是需要时间的,当加载成功后,会调用 onLoadComplete 方法, 在之前代码已经设置过相关的监听 。当再次进行播放,我们直接调用onLoadComplete方法,复用已加载的资源(在没有释放之前)

/**
 * 播放:如果资源还没有加载,则可能会有一小段等待时间
 */
private void playMayWait() {
    createSoundPoolIfNeeded();

    // mSoundId is invalid ,load from res raw for once before mSoundPool is released
    if (mSoundId == DEFAULT_INVALID_SOUND_ID) { 
        // 加载音频资源
        mSoundId = mSoundPool.load(getApplicationContext(), R.raw.autotest, 1); 
    } else {
        // reuse the loaded res
        if (mStreamID == DEFAULT_INVALID_STREAM_ID)
            // manually call this method when there is a valid mSoundId
            onLoadComplete(mSoundPool, 0, 0);  
    }
}

此处的load方法是从res/raw 下加载的,需要注意的是,通过改方式加载的资源会忽略后缀名,因此不同类型的资源不能重名. 实际上, 提供了多个load 方法, 以方便不同的需求:

 // Load the sound from the specified path.  根据路径加载
 public int load(String path, int priority)

// Load the sound from the specified APK resource  使用应用的资源
public int load(Context context, int resId, int priority)

// Load the sound from an asset file descriptor   使用 assert  文件描述符
public int load(AssetFileDescriptor afd, int priority) {

// Load the sound from a FileDescriptor.   使用文件描述符
public int load(FileDescriptor fd, long offset, long length, int priority)

通过 unload 方法来卸载之前加载的资源 ,参数为已加载过的资源 id

/**
 * Unload a sound from a sound ID.
 *
 * Unloads the sound specified by the soundID. This is the value
 * returned by the load() function. Returns true if the sound is
 * successfully unloaded, false if the sound was already unloaded.
 *
 * @param soundID a soundID returned by the load() function
 * @return true if just unloaded, false if previously unloaded
 */
public native final boolean unload(int soundID);

onLoadComplete方法是用来播放的合理位置,它的调用预示着资源已经就绪,可以进行播放了,调用 SoundPool 的 play 方法即可进行播放.

@Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
    if (mSoundPool != null) {
        if (mStreamID == DEFAULT_INVALID_STREAM_ID)
            mStreamID = mSoundPool.play(mSoundId, mCruLeftVolume, mCurRightVolume, 16, -1, 1.0f);
    }
}

SoundPool 的 play 方法参数较多,不过都比较简单. 当活跃的stream数量大于maxStreams,调用该方法可导致另一个播放声音被停止: 这个方法依据之前通过load方法加载得到的sound ID , 来播放一个指定的音频文件的声音.

通过注释就比较好的理解参数的内容:

  1. soundID load方法返回的值,指向某个已加载的音频资源
  2. leftVolume\rightVolume 用来这种左右声道的值.范围 0.0f ~ 1.0f
  3. priority 流的优先级
  4. loop 循环播放的次数, -1 表示无限循环
  5. rate 播放的速率 , 2 表示2倍速度

返回值streamID , 返回0则播放失败,否则成功。通过改id可以进一步进行控制. 比如 pause\resume

/**
 * Play a sound from a sound ID.
 *
 * Play the sound specified by the soundID. This is the value
 * returned by the load() function. Returns a non-zero streamID
 * if successful, zero if it fails. The streamID can be used to
 * further control playback. Note that calling play() may cause
 * another sound to stop playing if the maximum number of active
 * streams is exceeded. A loop value of -1 means loop forever,
 * a value of 0 means don't loop, other values indicate the
 * number of repeats, e.g. a value of 1 plays the audio twice.
 * The playback rate allows the application to vary the playback
 * rate (pitch) of the sound. A value of 1.0 means play back at
 * the original frequency. A value of 2.0 means play back twice
 * as fast, and a value of 0.5 means playback at half speed.
 *
 * @param soundID a soundID returned by the load() function
 * @param leftVolume left volume value (range = 0.0 to 1.0)
 * @param rightVolume right volume value (range = 0.0 to 1.0)
 * @param priority stream priority (0 = lowest priority)
 * @param loop loop mode (0 = no loop, -1 = loop forever)
 * @param rate playback rate (1.0 = normal playback, range 0.5 to 2.0)
 * @return non-zero streamID if successful, zero if failed
 */
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);
}

播放控制

streamID是对流的特定实例的引用,通过 SoundPool 的 pause\resume\stop 方法, 就可以对播放进行控制。

public void pause(View view) {
    if (mSoundPool != null) {
        mSoundPool.pause(mStreamID);
    }
}

public void resume(View view) {
    if (mSoundPool != null) {
        mSoundPool.resume(mStreamID);
    }
}

public void stop(View view) {
    if (mSoundPool != null) {
        mSoundPool.stop(mStreamID);
        mStreamID = DEFAULT_INVALID_STREAM_ID;
    }
}

播放音量调节

通过 SoundPool 的 setVolume 方法就可以设置指定 mStreamID 的流的左右声道的音量值. 如下,通过SeekBar 进行动态调节

private OnSeekBarChangeListenerAdapter seekBarListener = new OnSeekBarChangeListenerAdapter() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        super.onProgressChanged(seekBar, progress, fromUser);
        if(mSoundPool == null) return;
        float volume = progress * 1.0f / seekBar.getMax();
        switch (seekBar.getId()) {
            case R.id.id_sk_left_vloume:  // 设置左 volume
                mCruLeftVolume = volume;
                mSoundPool.setVolume(mStreamID, mCruLeftVolume, mCurRightVolume);
                break;
            case R.id.id_sk_right_vloume: // 设置又 volume
                mCurRightVolume = volume;
                mSoundPool.setVolume(mStreamID, mCruLeftVolume, mCurRightVolume);
                break;
        }
    }
};

资源释放

通过unload 方法来卸载之前load的资源 , 并通过 release 方法释放SoundPool占用的资源

/**
 * 释放资源
 */
private void releaseSoundPool() {
    if (mSoundPool != null) {
        mSoundPool.autoPause();
        mSoundPool.unload(mSoundId);
        mSoundId = DEFAULT_INVALID_SOUND_ID;
        mSoundPool.release();
        mSoundPool = null;
    }
}

注意事项

尽管 SoundPool 使用比较简单,但是还是有许多需要注意的地方:

实例

如下示例,实现 SoundPool 的播放及控制功能,以及其他调节功能,如下图,具体代码见最后链接:

1

源码