Android SoundPool 钢琴弹奏(停止播放做 FadeOut)

20 篇文章 1 订阅
5 篇文章 0 订阅

Android SoundPool 主要用于快速播放多个短音频,开发文档链接。应用场景如:钢琴弹奏 APP,可用 SoundPool 播放每个琴键的音频资源。

先看应用场景效果:
在这里插入图片描述

为使用方便,封装 SoundPool 如下:

package com.alan.audioio.audio;

import android.content.res.AssetFileDescriptor;
import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import com.alan.audioio.audio.common.APPContext;
import com.alan.audioio.audio.common.AudioConstants;
import com.alan.audioio.audio.exception.AudioException;
import com.alan.audioio.utils.ALog;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * Author: AlanWang4523.
 * Date: 2020/10/17 14:57.
 * Mail: alanwang4523@gmail.com
 */
public class AndroidSoundPool {
    private static final String TAG = AndroidSoundPool.class.getSimpleName();
    private static final int MSG_FADE_OUT = 1001;
    private static final int FADE_DURATION = 30;
    private static final int FADE_INTERVAL_TIME = 6;
    private static final float FADE_INTERVAL_VOLUME = (1.0f / (1.0f * FADE_DURATION / FADE_INTERVAL_TIME));

    private SoundPool mSoundPool;
    private int mMaxStreamCount;
    private ArrayList<Integer> mSoundIdList;
    private ArrayList<Integer> mPlayingIdList;
    private CountDownLatch mCountDownLatch;
    private Handler mHandler;
    private HandlerThread mHandlerThread;
    private float mCurPlayVolume = 1.0f;

    /**
     * 构造函数
     */
    public AndroidSoundPool(int maxStreamCount) {
        mMaxStreamCount = maxStreamCount;
        mSoundIdList = new ArrayList<>();
        mPlayingIdList = new ArrayList<>();

        mSoundPool = createSoundPool(mMaxStreamCount);
        mSoundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
            @Override
            public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
                ALog.d("onLoadComplete()--->>sampleId = " + sampleId + ", status = " + status);
                if (mCountDownLatch != null) {
                    mCountDownLatch.countDown();
                }
            }
        });

        mHandlerThread = new HandlerThread(TAG);
        mHandlerThread.start();
        mHandler = new InternalHandler(mHandlerThread.getLooper(), this);
    }

    /**
     * 创建 SoundPool
     * @param maxStream 同时播放的最大流数量
     * @return SoundPool
     */
    private SoundPool createSoundPool(int maxStream) {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
            SoundPool.Builder builder = new SoundPool.Builder();
            builder.setMaxStreams(maxStream);
            AudioAttributes.Builder attributesBuilder = new AudioAttributes.Builder();
            attributesBuilder.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC);
            attributesBuilder.setFlags(256);
            attributesBuilder.setUsage(AudioAttributes.USAGE_MEDIA);
            attributesBuilder.setLegacyStreamType(3);
            builder.setAudioAttributes(attributesBuilder.build());
            return builder.build();
        } else {
            return new SoundPool(maxStream, AudioManager.STREAM_MUSIC, 0);
        }
    }

    /**
     * 加载资源列表
     * @param audioPathList 要加载的音频资源列表
     * @return soundIDList
     * @throws AudioException 加载失败会抛出 AudioException
     */
    public List<Integer> load(List<String> audioPathList) throws AudioException {
        mSoundIdList.clear();
        ArrayList<Integer> soundIDList = new ArrayList<>();
        if (audioPathList == null) {
            return soundIDList;
        }

        mCountDownLatch = new CountDownLatch(audioPathList.size());
        for (String audioPath : audioPathList) {
            int soundID = this.load(audioPath);
            soundIDList.add(soundID);
        }
        try {
            mCountDownLatch.await(audioPathList.size() * 2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            // do nothing
        }
        return soundIDList;
    }

    /**
     * 加载资源文件
     * @param audioPath 音频资源路径,支持协议如下:
     *        assets://piano/A.m4a
     *        exfile:///sdcard/Alan/Audio/piano/A.m4a
     *        /sdcard/Alan/Audio/piano/A.m4a
     *
     * @return soundID,可以用于播放或 unload
     * @throws AudioException 加载失败抛出 AudioException
     */
    private int load(String audioPath) throws AudioException {
        int soundID;
        String realPath;
        if (AudioConstants.isAssetsPath(audioPath)) {
            // assets 文件
            realPath = audioPath.replace(AudioConstants.HOST_ASSETS, "");
            try {
                AssetFileDescriptor assetFileDescriptor = APPContext.getAssetManager().openFd(realPath);
                soundID = mSoundPool.load(assetFileDescriptor, 0);
            } catch (IOException e) {
                throw new AudioException("Load asset file failed.", e);
            }
        } else if (AudioConstants.isExFilePath(audioPath)) {
            // 外部存储文件
            realPath = audioPath.replace(AudioConstants.HOST_EXFILE, "");
            soundID = mSoundPool.load(realPath, 0);
        } else {
            // 其他绝对路径不带前缀的文件
            realPath = audioPath;
            soundID = mSoundPool.load(realPath, 0);
        }
        mSoundIdList.add(soundID);
        return soundID;
    }

    /**
     * 播放某个资源
     * @param soundID soundID,由 {@link #load(String)} 返回
     */
    public void play(int soundID) {
        mCurPlayVolume = 1.0f;
        int playingId = mSoundPool.play(soundID,
                1.0f, 1.0f, 0, 0, 1.0f);
        synchronized (AndroidSoundPool.this) {
            if ((playingId != 0) && !mPlayingIdList.contains(playingId)) {
                mPlayingIdList.add(playingId);
            }
            if (mPlayingIdList.size() > mMaxStreamCount) {
                mPlayingIdList.remove(0);
            }
        }
    }

    /**
     * 停止播放,停止时会做 fade out
     */
    public void stopPlay() {
        mHandler.removeMessages(MSG_FADE_OUT);
        mHandler.sendEmptyMessage(MSG_FADE_OUT);
        try {
            Thread.sleep(FADE_DURATION + FADE_INTERVAL_TIME);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 卸载某个资源
     * @param soundID soundID,由 {@link #load(String)} 返回
     */
    public void unload(int soundID) {
        mSoundPool.unload(soundID);
        int idIndex = -1;
        for (int i = 0; i < mSoundIdList.size(); i++) {
            if (soundID == mSoundIdList.get(i)) {
                idIndex = i;
            }
        }
        if (idIndex >= 0) {
            mSoundIdList.size();
            mSoundIdList.remove(idIndex);
        }
    }

    /**
     * 卸载所有资源
     */
    public void unloadAll() {
        for (Integer soundID : mSoundIdList) {
            mSoundPool.unload(soundID);
        }
        mSoundIdList.clear();
    }

    /**
     * 释放资源
     */
    public void release() {
        unloadAll();
        mSoundPool.release();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mHandlerThread.quitSafely();
        } else {
            mHandlerThread.quit();
        }
    }

    private void handleFadeOut() {
        mCurPlayVolume -= FADE_INTERVAL_VOLUME;
        setVolume(mCurPlayVolume);
        if (mCurPlayVolume > 0) {
            mHandler.sendEmptyMessageDelayed(MSG_FADE_OUT, FADE_INTERVAL_TIME);
        } else {
            synchronized (AndroidSoundPool.this) {
                for (Integer playingId : mPlayingIdList) {
                    mSoundPool.stop(playingId);
                }
                mPlayingIdList.clear();
            }
        }
    }

    private void setVolume(float volume) {
        ALog.d("setVolume()----->>>" + volume + ", PlayingIdList = " + mPlayingIdList.toString());
        if (volume > 1.0f) {
            volume = 1.0f;
        } else if (volume < 0.01f) {
            volume = 0.0f;
        }
        synchronized (AndroidSoundPool.this) {
            try {
                for (Integer playingId : mPlayingIdList) {
                    mSoundPool.setVolume(playingId, volume, volume);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private static class InternalHandler extends Handler {
        private WeakReference<AndroidSoundPool> weakRefSoundPool;
        public InternalHandler(Looper looper, AndroidSoundPool androidSoundPool) {
            super(looper);
            weakRefSoundPool = new WeakReference<>(androidSoundPool);
        }

        @Override
        public void handleMessage(Message msg) {
            AndroidSoundPool androidSoundPool = weakRefSoundPool.get();
            if (androidSoundPool == null) {
                return;
            }
            if (msg.what == MSG_FADE_OUT) {
                androidSoundPool.handleFadeOut();
            }
        }
    }
}

调用逻辑如下:

package com.alan.audioio.app;

import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import com.alan.audioio.R;
import com.alan.audioio.app.ui.PianoKeyItemView;
import com.alan.audioio.audio.AndroidSoundPool;
import com.alan.audioio.audio.common.APPContext;
import com.alan.audioio.audio.exception.AudioException;
import com.alan.audioio.utils.ALog;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

/**
 * Author: AlanWang4523.
 * Date: 2020/10/17 18:38.
 * Mail: alanwang4523@gmail.com
 */
public class TestSoundPoolActivity  extends AppCompatActivity implements View.OnClickListener {

    public static void launchMe(Context context) {
        context.startActivity(new Intent(context, TestSoundPoolActivity.class));
    }

    private static final String MUSIC_PIANO_DIR = "assets://piano/";
    private static final String[] KEY_NAMES = {"A", "B", "C", "D", "E",};
    private static final String FILE_SUFFIX = ".m4a";

    private static final int PIANO_KEYS_COUNT = 5;
    private static final int MAX_SOUND_COUNT = 5;
    private int[] btnPianoKeysIdArr;// 按钮id
    private PianoKeyItemView[] pianoKeyItemViewArr;
    private HashMap<Integer, Integer> btnIdIndexMap = new HashMap<>(PIANO_KEYS_COUNT);
    private HashMap<Integer, Integer> btnIdAndSoundIdMap = new HashMap<>(PIANO_KEYS_COUNT);
    private TextView btnStopPlay;
    private ProgressDialog mProgressDialog;
    private AndroidSoundPool mAndroidSoundPool;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sound_pool);

        btnStopPlay = findViewById(R.id.btn_stop);
        btnStopPlay.setOnClickListener(this);

        APPContext.getInstance().setContext(this);

        btnPianoKeysIdArr = new int[PIANO_KEYS_COUNT];
        btnPianoKeysIdArr[0] = R.id.btn_key_A;
        btnPianoKeysIdArr[1] = R.id.btn_key_B;
        btnPianoKeysIdArr[2] = R.id.btn_key_C;
        btnPianoKeysIdArr[3] = R.id.btn_key_D;
        btnPianoKeysIdArr[4] = R.id.btn_key_E;

        pianoKeyItemViewArr = new PianoKeyItemView[PIANO_KEYS_COUNT];
        for (int i = 0; i < pianoKeyItemViewArr.length; i++) {
            btnIdIndexMap.put(btnPianoKeysIdArr[i], i);
            pianoKeyItemViewArr[i] = findViewById(btnPianoKeysIdArr[i]);
            pianoKeyItemViewArr[i].setOnClickListener(this);
        }

        loadMusicInstrument();
    }

    private void loadMusicInstrument() {
        ALog.e("loadMusicInstrument--------------->>");

        if (mProgressDialog == null) {
            mProgressDialog = new ProgressDialog(this);
            mProgressDialog.setMessage("正在加载乐器...");
        }
        mProgressDialog.show();

        if (mAndroidSoundPool != null) {
            mAndroidSoundPool.release();
        }
        mAndroidSoundPool = new AndroidSoundPool(MAX_SOUND_COUNT);

        final ArrayList<String> audioFileList = new ArrayList<>();
        for (int i = 0; i < pianoKeyItemViewArr.length; i++) {
            audioFileList.add(getAudioPath(i));
        }

        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    List<Integer> soundIdList = mAndroidSoundPool.load(audioFileList);
                    for (int i = 0; i < soundIdList.size(); i++) {
                        ALog.e("loadAudioAsync:: i = " + i + ", soundId = " + soundIdList.get(i)
                                + ", keyPath = " + getAudioPath(i));
                        btnIdAndSoundIdMap.put(btnPianoKeysIdArr[i], soundIdList.get(i));
                    }
                    TestSoundPoolActivity.this.runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            mProgressDialog.dismiss();
                        }
                    });
                } catch (AudioException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    private String getAudioPath(int i) {
        return MUSIC_PIANO_DIR + KEY_NAMES[i] + FILE_SUFFIX;
    }

    @Override
    protected void onDestroy() {
        if (mProgressDialog != null && mProgressDialog.isShowing()) {
            mProgressDialog.dismiss();
        }
        if (mAndroidSoundPool != null) {
            mAndroidSoundPool.stopPlay();
            mAndroidSoundPool.release();
        }
        super.onDestroy();
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.btn_stop) {
            if (mAndroidSoundPool != null) {
                mAndroidSoundPool.stopPlay();
            }
        } else {
            int index = btnIdIndexMap.get(view.getId());
            if (index >= 0) {
                PianoKeyItemView keyItemView = pianoKeyItemViewArr[index];
                int soundId = btnIdAndSoundIdMap.get(view.getId());
                ALog.e("PlayPiano--->> " + keyItemView.getKeyName() + ", soundId = " + soundId);
                if (mAndroidSoundPool != null) {
                    mAndroidSoundPool.play(soundId);
                }
            }
        }
    }
}

完整 Demo

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值