AudioTrack播放PCM文件(一)

前言

在开始如何在安卓系统渲染原始PCM音频数据之前,先学习和了解与平台有关的基础知识。在安卓设备,一般的播放MP3格式音频,不管网络的还是本地的,通过MediaPlayer即可很方便实现,这里不再阐述。但是在直播应用场景中,都是渲染PCM裸音频数据,如何实现呢?有两种方式,方式一、通过Java层API AudioTrack渲染;方式二、通过native层的OpenSL ES渲染,OpenSL ES是嵌跨平台的嵌入式音频处理库,目前安卓系统接入的Open SL ES版本是1.0.1,这两种方式都是比较底层的API,需要比较多的配置方式,同时也比较灵活。

准备知识

.采样率
采集音频数据的频率。通俗点讲就是一秒钟有多少次采样,采样频率一般从8000hz-48000hz;
.采样位宽
每个采样的音频数据的表示精度,一般有8位,16位,32位;
.声道
单声道,双声道,多声道等等。单声道一次采样对应一个音频数据,双声道则一次采样对应两个音频数据,多声道则一次采样对应多个音频数据;拿44100hz采样率来说,如果采样位宽为16、1秒钟的音频数据大小为:44100x2x2个字节
.音频帧
这个概率是对于音频编码和音频渲染来说的。它表示一块音频数据(通常包含多个音频数据);对于音频编码来说,音频帧指的是每次编码包含的音频数据个数,不同的编码方式,每个音频帧包含的音频数据个数也不一样,比如aac编码一帧包含1024个音频数据,MP3编码一帧则包含1152个音频数据;对于音频渲染来说,音频帧指的是app一次发送给音频渲染系统的音频数据个数,具体个数由APP自己决定,一般取10-20ms数据为宜。
.音频存储序
当采用位宽为16位、32位时,就涉及到音频存储序的问题。可以采用大端序,也可以采用小端序。对于安卓渲染来说,只能播放小端序,ios则不受限制
.重采样之降采样和升采样
改变一段音频的采样率成为重采样。改变之后采样率大于原来的采样率成为上采样,小于则成为下采样。重采样是通过特点的插值算法增加或减少采样音频数据的过程,所以重采样往往会造成音频质量的损失。

本文介绍如何通过AudioTrack来播放PCM裸音频数据

目标

用AudioTrack渲染PCM音频,这里播放的PCM文件是通过ffmpeg解码到文件后的裸数据进行模拟

使用步骤

1、初始化AudioTrack
方式一:
这是官方推荐的初始化方法

/** 初始化AudioTrack,官方推荐此方法
     * ch_layout:声道类型
     * format:采样格式 表示一个采样点使用多少位表示
     * sampleRate:一般有20khz,44.1khz 48khz等
     * */
    private void initAudioTrackByBuilder(int ch_layout,int sampleRate,int format) {
        mMinBufferSize = AudioTrack.getMinBufferSize(sampleRate, ch_layout, format);
        aTrack = new AudioTrack.Builder()
                // AudioAttributes用来设置音频类型,相当于上面的streamType,如下是播放音频的策略
                .setAudioAttributes(new AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_MEDIA)
                        .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)// 这两项一般都是对应设置
                        .build())
                .setAudioFormat(new AudioFormat.Builder()
                        .setEncoding(format)        // 采样格式
                        .setSampleRate(sampleRate)  // 采样率
                        .setChannelMask(ch_layout)  // 就是声道类型
                        .build())
                .setBufferSizeInBytes(mMinBufferSize)
                .build();
    }

mMinBufferSize = AudioTrack.getMinBufferSize(sampleRate, ch_layout, format);
计算最小的缓冲区的大小,官方建议使用此方式计算,而非自己手动计算
方式二:

/** 初始化AudioTrack
     * ch_layout:声道类型
     * format:采样格式 表示一个采样点使用多少位表示
     * sampleRate:一般有20khz,44.1khz 48khz等
     * */
    private void initAudioTrack(int ch_layout,int sampleRate,int format) {
        // 最好使用此函数计算缓冲区大小,而非自己手动计算
        mMinBufferSize = AudioTrack.getMinBufferSize(sampleRate, ch_layout, format);

        /**
         * int streamType:表示了不同的音频播放策略,按下手机的音量键,可以看到有多个音量管理,比如可以单独禁止警告音但是可以开启
         * 乐播放声音,这就是不同的音频播放管理策略;以常量形式定义在AudioManager中,如下:
         *      STREAM_MUSIC:播放音频用这个就好
         *      STREAM_VOICE_CALL:电话声音
         *      STREAM_ALARM:警告音
         *      ......
         * int sampleRateInHz:音频采样率
         * int channelConfig:声道类型;CHANNEL_IN_XXX适用于录制音频,CHANNEL_OUT_XXX用于播放音频
         * int audioFormat:采样格式
         * int bufferSizeInBytes:音频会话的缓冲区大小。音频播放时,app将音频原始数据不停的输送给这个缓冲区,然后AudioTrack不停从这个缓冲区拿数据送给音频播放系统
         * 从而实现声音的播放
         * int mode:缓冲区数据的流动方式;如下:
         * MODE_STREAM:流式流动,只缓存部分
         * MODE_STATIC:一次性缓冲全部数据,适用于音频比较小的播放
         * 备注:对于录制音频,为了性能考虑,最好用CHANNEL_IN_MoNo单声道,而转变立体声的过程在声音的特效处理阶段来完成
         * */
        aTrack = new AudioTrack(
                AudioManager.STREAM_MUSIC,// 指定流的类型
                sampleRate,// 设置音频数据的採样率 32k,假设是44.1k就是44100
                ch_layout,// 设置输出声道为双声道立体声,而CHANNEL_OUT_MONO类型是单声道
                format,// 设置音频数据块是8位还是16位。这里设置为16位。
                mMinBufferSize,//缓冲区大小
                AudioTrack.MODE_STREAM // 设置模式类型,在这里设置为流类型,第二种MODE_STATIC貌似没有什么效果
        );
    }

2、启动播放
AudioTrack初始化之后就可以调用播放接口开始播放音频数据了

// 将AudioTrack切换到播放状态
    public void play() {
        isStop = false;
        if (aTrack != null && aTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
            aTrack.play();
            startAudioThread();
        }
    }

3、输送音频数据
要想实现流畅播放,输送音频数据必须单独一个线程中,如下:

private class AudioThread implements Runnable {
        @Override
        public void run() {
            DDlog.logd("AudioThread start mMinBufferSize==> "+mMinBufferSize);
            // 一次写入的数据可以是1024,不一定非得mMinBufferSize个字节
//            samples = new short[mMinBufferSize];
            while (!isStop) {
                try {
                    byte[] buffer = new byte[1024];
                    int sampleSize = mInputStream.read(buffer);
//                    DDlog.logd(ByteUtil.byte2hex(buffer));

                    // 向缓冲区写入数据,此函数为阻塞行数,一般写入200ms数据需要接近200ms时间
                    if (mFormat == AudioFormat.ENCODING_PCM_FLOAT) {
                        float[] samples = ByteUtil.bytesToFloats(buffer, sampleSize, isBigendian);
                        aTrack.write(samples,0,samples.length,AudioTrack.WRITE_BLOCKING);
                    } else if (mFormat == AudioFormat.ENCODING_PCM_16BIT) {
                        short[] samples = ByteUtil.bytesToShorts(buffer, sampleSize, isBigendian);
                        aTrack.write(samples, 0, samples.length);
                    } else {
                        aTrack.write(buffer, 0, sampleSize);
                    }

                } catch (IOException io) {

                }
            }
        }
    }

这里要注意的一点就是,如果是直接从PCM文件中音频存储序是大端序方式,则还需要转换为小端序;并且还要将bytes[]数组转换成对应的shorts[]数组(如果播放16位音频),floats[]数组(如果播放32位float音频),转换代码如下:

// 将byte[] 数组转换成short[]数组
    public static short[] bytesToShorts(byte[] bytes,int len,boolean isBe) {
        if(bytes==null){
            return null;
        }
        short[] shorts = new short[len/2];
        // 大端序
        if (isBe) {
            ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asShortBuffer().get(shorts);
        } else {
            ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shorts);
        }

        return shorts;
    }

    // 将byte[] 数组转换成float[]数组
    public static float[] bytesToFloats(byte[] bytes,int len,boolean isBe) {
        if(bytes==null){
            return null;
        }
        float[] floats = new float[len/4];
        // 大端序
        if (isBe) {
            ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN).asFloatBuffer().get(floats);
        } else {
            ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer().get(floats);
        }

        return floats;
    }

4、停止播放

// 停止播放
if (aTrack != null && aTrack.getState() != AudioTrack.STATE_UNINITIALIZED) {
     aTrack.stop();
}
// 释放AudioTrack
        aTrack.release();
        aTrack = null;

总结:

.遇到问题:
1、对于输入数据为大端序无法正常播放
.解决方案:
AudioTrack只能播放小端序的音频数据,所以对于大端序的数据得先转换成小端序在播放
2、输入数据格式为float类型无法正常播放
.解决方案:
由于inputStream读取的是字节,而当播放float类型数据时,write()函数写入的必须是float数组,所以写入之前要将byte[]数据转换成float[]数据
3、安卓对于8位数据的渲染不正常

项目地址

Demo

你可能感兴趣的:(AudioTrack播放PCM文件(一))