Android蓝牙开发系列文章-AudioTrack播放PCM音频

终于迎来了蓝牙a2dp的第二篇:利用AudioTrack播放PCM音频数据。如想查看更多内容,请点击《Android蓝牙开发系列文章-策划篇》。

先回顾一下上一篇文章《Android蓝牙开发系列文章-蓝牙音箱连接》讲到的蓝牙音箱的完成配对、连接的流程:扫描设备--监听DEVICE_FOUND广播-->直到找到目标设备-->对目标设备发起配对-->监听到设备配对成功-->发起设备连接-->监听连接状态的广播,连接成功。

本篇基于上一节的小结果,实现播放PCM数据,蓝牙音箱出声音。

音乐播放器播放的声音数据,经过解码、混音等处理后,送给a2dp_hw的数据就是PCM数据,也是这点联系,所以写了这篇文章。

目录

 

1.常见的音乐播放方式有哪些?

2.利用AudioTrack实现播放音频

2.1.配置基本参数

2.2获取最小缓冲区大小

2.3 基于基本参数、缓冲区创建AudioTrack对象

2.4 读取PCM文件,转成DataInputStream

2.5开启/停止播放


1.常见的音乐播放方式有哪些?

我了解到的常见的音乐播放方式有如下三种,如果你知道更多,请留言告诉我哈~

方式 特点
SoundPool 适用于播放短促的声音,例如游戏音效、按键音等
AudioTrack 仅适用于播放PCM音频数据
MediaPlayer 能够播放多种文件格式的音频数据,例如MP3/AAC/WAV、OGG等。MediaPlayer在framework层创建AudioTrack,音频数据经过解码得到PCM数据,PCM数据再送到AudioTrack

 

一个应用同一时刻可以创建多个AudioTrack,每个AudioTrack会注册到AudioFlinger中,所以应用的AudioTrack传输到AudioFlinger 中完成混音,然后输送到AudioHardware中进行播放。

Android同一时候最多能够创建32个音频流。

2.利用AudioTrack实现播放音频

  1. 配置基本参数
  2. 获取最小缓冲区大小
  3. 基于基本参数、缓冲区创建AudioTrack对象
  4. 读取PCM文件,转成DataInputStream
  5. 开启/停止播放

 2.1.配置基本参数

先上一下代码:

    //设置音频流类型
    private static final int mStreamType = AudioManager.STREAM_MUSIC;
    //指定采样率
    private static final int mSampleRateInHz=44100 ;
    //指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量
    private static final int mChannelConfig= AudioFormat.CHANNEL_IN_STEREO; //双声道
    //指定音频量化位数
    private static final int mAudioFormat=AudioFormat.ENCODING_PCM_16BIT;
    //因为我们的PCM文件较大,所欲选择载入方式为MODE_STREAM
    private static int mMode = AudioTrack.MODE_STREAM;

音频流类型: 

音频流常见的有如下几种,这里引出一个问题:为什么要区分出来这么多种类型?可能这样做的好处有很多哈,我get到的一个好处是:这样可以实现多不同场景下的音频数据进行区分控制,例如,在播放音乐时调小了音量,这个操作会对AudioManager.STREAM_MUSIC产生影响,但不会对其他类型音频起作用,假设这个时候有电话进来,电话铃声的音量跟播放音乐的音量是不一样的。

类型 解释
AudioManager.STREAM_MUSIC 用于音乐播放的音频流
AudioManager.STREAM_SYSTEM 用于系统声音的音频流
AudioManager.STREAM_RING 用于电话铃声的音频流
AudioManager.STREAM_VOICE_CALL 用于电话通话的音频流
AudioManager.STREAM_ALARM 用于电话警报的音频流
AudioManager.STREAM_NOTIFICATION 用于通知的音频流
AudioManager.STREAM_BLUETOOTH_SCO 用于连接蓝牙耳机时的音频流
AudioManager.STREAM_TTS 用于文本到音频转换的音频流

采样频率:

是指一秒钟内对模拟信号的采样次数,结合本文来说,就是获取到这个PCM音频实的采样频率,采样频率越高,越能够还原原始数据,但是会使得采样得到的文件体积更大。

奈奎斯特采样理论:当对被采样的模拟信号进行还原时,其最高频率只有采样频率的一半,换句话说,如果我们要完整还原原始的模拟信号,则采样频率就必须是它的两倍以上。

由于人耳所能辨识的声音范围是20-20KHZ,所以人们一般都选用44.1KHZ(CD)、48KHZ或者96KHZ来做为采样频率。

采样位数
它是用来衡量声音波动变化的一个参数,也可以说是声卡的分辨率。它的数值越大,分辨率也就越高,所发出声音的能力越强。下图是采样位数为4位的示意图,数值范围为0~15,采样数值分别为7,9,11,12,13,14,15,14.......。到采样位数越大时,能够表达的数值范围也就越大,量化得到的值也就越接近原始数据。

音频载入方式:

有两种音频载入方式,他们之间的区别如下:

音频载入方式 优缺点
AudioTrack.MODE_STREAM 将PCM数据分一次次放入AudioTrack,适用于大的PCM数据,会产生一定的延迟问题
AudioTrack.MODE_STATIC 将PCM数据一次性放入AudioTrack,适用于小的PCM数据和要求时延小的场景

看到这里可以在回头看一下上面的代码,看是否都以理解了上面的概念。此时,你也会问:你怎么知道这些参数取什么值合适的?

其实,我是利用Cool Edit Pro这个工具试出来的,打开Cool Edit Pro,选择文件找到我们的PCM文件,然后选择一组采样格式,然后点击“>”开始播放,听一下声音是否正常,如果正常,则说明你选择的参数是合理的(但不一定是最佳)。

  2.2获取最小缓冲区大小

这个缓冲区大小一定要通过AudioTrack::getMinBufferSize()来获取,一定不要自己附一个值。这个值与采样率、通道数、采样位数有关,具体计算公式在这里就不细究了,毕竟本专题是讲解蓝牙的~

mMinBufferSize = AudioTrack.getMinBufferSize(mSampleRateInHz,mChannelConfig, mAudioFormat);//计算最小缓冲区

2.3 基于基本参数、缓冲区创建AudioTrack对象

mAudioTrack = new AudioTrack(mStreamType, mSampleRateInHz,mChannelConfig,
                mAudioFormat,mMinBufferSize,mMode);
Log.d(TAG, "intData, mAudioTrack.getState() = " + mAudioTrack.getState());

 可以看到AudioTrack有三种状态,在创建后就处于STATE_INITIALIZED状态,也就是说明:上面的log输出为:

03-13 23:37:45.647 12027-12027/com.atlas.btdemo D/MusicPlayer: intData, mAudioTrack.getState() = 1

  /**
     * State of an AudioTrack that was not successfully initialized upon creation.
     */
    public static final int STATE_UNINITIALIZED = 0;
    /**
     * State of an AudioTrack that is ready to be used.
     */
    public static final int STATE_INITIALIZED   = 1;
    /**
     * State of a successfully initialized AudioTrack that uses static data,
     * but that hasn't received that data yet.
     */
    public static final int STATE_NO_STATIC_DATA = 2;

2.4 读取PCM文件,转成DataInputStream

我们在res目录下创建一个名为raw的文件夹,然后将pcm文件放进去,在代码中访问该文件的方法如下:

private DataInputStream mDis;//播放文件的数据流
mDis = new DataInputStream(context.getResources().openRawResource(R.raw.pcm_test));

因为我用PCM文件有10M多,在安装应用的过程中十分耗时,这一点很不好,不知道有什么办法可以解决? 

2.5开启/停止播放

我们采用一个最笨的办法:自己记录一个标记,在开始播放时,将该标记设置成true,在停止播放时,将该标记设置成false。

private boolean isStart = false;
public boolean isMusicPlaying() {
        if(isStart == true) {
            return true;
        } else {
            return false;
        }
 }

 /**
     * 销毁线程方法
     */
private void destroyThread() {
        try {
            isStart = false;
            if (null != mRecordThread && Thread.State.RUNNABLE == mRecordThread.getState()) {
                try {
                    Thread.sleep(500);
                    mRecordThread.interrupt();
                } catch (Exception e) {
                    mRecordThread = null;
                }
            }
            mRecordThread = null;
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            mRecordThread = null;
        }
    }

    /**
     * 启动播放线程
     */
    private void startThread() {
        destroyThread();
        isStart = true;
        if (mRecordThread == null) {
            mRecordThread = new Thread(recordRunnable);
            mRecordThread.start();
        }
    }

    /**
     * 播放线程
     */
    Runnable recordRunnable = new Runnable() {
        @Override
        public void run() {
            try {
                //设置线程的优先级
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
                byte[] tempBuffer = new byte[mMinBufferSize];
                int readCount = 0;
                while (mDis.available() > 0) {
                    readCount= mDis.read(tempBuffer);
                    if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) {
                        continue;
                    }
                    if (readCount != 0 && readCount != -1) {//一边播放一边写入语音数据
                        //判断AudioTrack未初始化,停止播放的时候释放了,状态就为STATE_UNINITIALIZED
                        if(mAudioTrack.getState() == mAudioTrack.STATE_UNINITIALIZED){
                            initData();
                        }
                        mAudioTrack.play();
                        mAudioTrack.write(tempBuffer, 0, readCount);
                    }
                }
                stopPlay();//播放完就停止播放
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    };

其实还有一个更好的办法是通过如下接口来判断,

 /**
     * Returns the playback state of the AudioTrack instance.
     * @see #PLAYSTATE_STOPPED
     * @see #PLAYSTATE_PAUSED
     * @see #PLAYSTATE_PLAYING
     */
    public int getPlayState() {
        synchronized (mPlayStateLock) {
            switch (mPlayState) {
                case PLAYSTATE_STOPPING:
                    return PLAYSTATE_PLAYING;
                case PLAYSTATE_PAUSED_STOPPING:
                    return PLAYSTATE_PAUSED;
                default:
                    return mPlayState;
            }
        }
    }

那什么时刻触发音乐播放或者停止呢?

我们在蓝牙音箱连接成功后,就去开始播放音乐。在点击UI上的PLAY_PCM实现音乐播放状态的反转:播放-》暂停,或者暂停-》播放。

if (action.equals(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) {
                BluetoothDevice btdevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                int preConnectionState = intent.getIntExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, 0);
                int newConnectionState = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, 0);
                Log.d(TAG, "btdevice = " + btdevice.getName() + ", preConnectionState = "
                        + preConnectionState + ", newConnectionState" + newConnectionState);
                if(newConnectionState == BluetoothProfile.STATE_CONNECTED && preConnectionState == BluetoothProfile.STATE_CONNECTING) {
                    Log.d(TAG, "target device connect success");
                    if(mMusicPlayer != null && mMusicPlayer.isMusicPlaying()) {
                        Log.d(TAG, "music is playing");
//                        mHandler.sendEmptyMessage(MSG_START_PLAYPCM);
                        mHandler.sendEmptyMessageDelayed(MSG_STOP_PLAYPCM, DELAYT_TIMES);
                    } else {
                        Log.d(TAG, "music play has stopped");
//                        mHandler.sendEmptyMessage(MSG_STOP_PLAYPCM);
                        mHandler.sendEmptyMessageDelayed(MSG_START_PLAYPCM,DELAYT_TIMES);
                    }
                }
            }

 @Override
    public void onClick(View view) {
        Log.d(TAG, "view id = " + view.getId());
        switch (view.getId()) {
            case R.id.bt_scan:
                Log.d(TAG, "start bt scan");
                mHandler.sendEmptyMessageDelayed(MSG_SCAN, DELAYT_TIMES);
                break;
            case R.id.bt_connect:
                initProfileProxy();
                Log.d(TAG, "start bt connect");
                mHandler.sendEmptyMessageDelayed(MSG_CONNECT, DELAYT_TIMES);
                break;
            case R.id.bt_playpcm:
                if(mMusicPlayer != null && mMusicPlayer.isMusicPlaying()) {
                    Log.d(TAG, "music is playing");
//                    mHandler.sendEmptyMessage(MSG_START_PLAYPCM);
                    mHandler.sendEmptyMessageDelayed(MSG_STOP_PLAYPCM, DELAYT_TIMES);
                } else {
                    Log.d(TAG, "music play has stopped");
//                    mHandler.sendEmptyMessage(MSG_STOP_PLAYPCM);
                    mHandler.sendEmptyMessageDelayed(MSG_START_PLAYPCM, DELAYT_TIMES);
                }
                break;
            default:
                break;
        }

本想给大家录一段音频,可是娃都睡了,就不录了~

如果想持续关注本博客内容,请扫描关注个人微信公众号,或者微信搜索:万物互联技术。

发布了35 篇原创文章 · 获赞 17 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Atlas12345/article/details/104849584