音视频开发之旅(三)AudioTrack播放PCM音频

目录

  1. AudioTrack和MediaPlayer
  2. AudioTrack的API介绍(构造、操作、状态机)
  3. 具体实现(Static和Stream两种模式)
  4. 遇到的问题
  5. 收获

一、MediaPlayer和AudioTrack

Android SDK 中提供了三种播放声音的API,常见的是MediaPlayer和AudioTrack
其中AudioTrack管理、播放单一音频资源。可以将PCM音频数据传输到音频接收器,以供播放,只能播放源码流即PCM,wav封装格式的音频也可以用AudioTrack播放,但是wav头部分在播放解析是会发出噪音。
而MediaPlayer可以播放多种格式的音频文件,比如 mp3 aac等,因为MediaPlayer会在framework层创建对应的音频解码器。
既然MediaPlayer可以播放那么多的音频格式,为什么我们还要学习AudioTrack呐?
首先MediaPlayer在framwork层还是会创建AudioTrack,把解码后的PCM流传递给AudioTrack,再传递给AudioFliger进行混音播放。

每一个音频流对应一个AudioTrack,AudioTrack会在创建时注册到AudioFlinger中,AudioFlinger把所有的AudioTrack进行混合Mixer,然后输送到AudioHardware进行播放。Android最多可以同时创建32个音频流。在短视频编辑等应用领域,对视频进行添加配乐进行编辑,需要把视频中的音轨和配乐中的音轨进行解码PCM进行混合再编码。再或者我们在剪映等视频编辑app可以添加多个音轨,就想Audition一样强大。这些都都需要我们对AudioTrack有一定的了解掌握。

二、AudioTrack的介绍

我们先简单看下AudioTrack提供了哪些API

2.1. 构造方法

public AudioTrack(int streamType, int sampleRateInHz, int channelConfig, int audioFormat,
int bufferSizeInBytes, int mode)
其中采样率sampleRateInHz、声道数channelConfig、音频格式audioFormat以及音频缓冲区大小bufferSizeInBytes 这四个概念和上一篇《AudioRecord录制PCM音频》中的介绍的AudioRecord的构造方法的参数意义以及获取方式基本一致。下面我们看下另外两个参数streamType以及mode

streamType音频流的类型,有如下几种
AudioManager#STREAM_VOICE_CALL:电话声音AudioManager#STREAM_SYSTEM:系统声音
AudioManager#STREAM_RING:铃声
AudioManager#STREAM_MUSIC:音乐声
AudioManager#STREAM_ALARM:闹铃声
AudioManager#STREAM_NOTIFICATION:通知声

这里我们使用的是AudioManager#STREAM_MUSIC。_
下面我们重点看下mode
@param mode streaming or static buffer.
MODE_STATIC and MODE_STREAM

  • STATIC模式:一次性将所有的数据放到一个固定的buffer,然后直接传送给AudioTrack,简单有效,通常应用于播放铃声或者系统提示音等,占用内存较少的音频数据
  • STREAM模式:一次一次的将音频数据流写入到AudioTrack对象中,并持续处于阻塞状态,当数据从Java层到Native层执行播放完毕后才返回,这种方式可以避免由于音频过大导致内存占用过多。当然对应的不足就是总是在java和native层进行交互,并且阻塞知道播放完毕,效率损失较大。

2.2. Action 写入、播放、暂停、停止、释放

write(byte audioData, int offsetInBytes, int sizeInBytes)把pcm数据写入到AudioTrack对象
播放、暂停、停止、释放 常规的播放Action操作。

2.3. 状态机(getState以及getPlayState)

AudioTrack中有两个state,一个是AudioTrack是否已经初始化,后续的Action操作都依赖于此,这个有点类似MediaPlayer的prepared状态,只有处于prepared状态之后才可以进行其他播放相关操作
另外一个就是playstate,用于记录判断当前处于什么播放状态。
状态的改变加速,处理多线程同步问题
private final Object mPlayStateLock = new Object();

三、具体实现

我们那上一篇《AudioRecord录制PCM音频》中示例代码产生的pcm作为AudioTrack的数据播放源来。跟进mode不同,分别实现

3.1 STATIC模式

//1. 初始化参数和buffer
   
 private void initAudioTrackParams() {
        sampleRateInHz = 44100;
        channels = AudioFormat.CHANNEL_OUT_MONO;//错误的写成了CHANNEL_IN_MONO
        audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channels, audioFormat);

        pcmFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "raw.pcm");
        if (pcmFile.exists()) {
            hasPcmFile = true;
        }
    }


private void initStaticBuff() {

        //staic模式是一次读取全部的数据,在play之前要先完成{@link audioTrack.write()}


        if (audioTrackThread != null) {
            audioTrackThread.interrupt();
        }

        audioTrackThread = new Thread(new Runnable() {
            @Override
            public void run() {
                FileInputStream fileInputStream = null;
                try {
                    //init audioTrack 需要先确定buffersize
                    fileInputStream = new FileInputStream(pcmFile);
                    long size = fileInputStream.getChannel().size();
                    staicBuff = new byte[(int) size];

                    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(staicBuff.length);
                    int byteValue = 0;
                    long startTime = System.currentTimeMillis();
                    while ((byteValue = fileInputStream.read()) != -1) {
//                        Log.d(TAG, "run: " + byteValue);
                        //耗时操作
                        byteArrayOutputStream.write(byteValue);
                    }
                    Log.d(TAG, "byteArrayOutputStream write Time: " + (System.currentTimeMillis() - startTime));
                    staicBuff = byteArrayOutputStream.toByteArray();

                    isReadying = true;

                } catch (IOException e) {
                    e.printStackTrace();
                } catch (Throwable e) {
                    e.printStackTrace();
                } finally {
                    if (fileInputStream != null) {
                        try {
                            fileInputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    Log.d(TAG, "playWithStaicMode: end");
                }
            }
        });
        audioTrackThread.start();

    }

//. 2. 点击播放

private void play(byte[] staicBuff) {
        //1. static模式是一次读去pcm到内存,比较耗时,只有读取完之后才可以调用play
        if (!isReadying) {
            Toast.makeText(this, "请稍后", Toast.LENGTH_SHORT).show();
            return;
        }

        //2.如果正在播放中,重复点击播放,则停止当次播放,调用reloadStaticData重新加载数据,然后play
        if (isPlaying) {
            audioTrack.stop();
            audioTrack.reloadStaticData();
            Log.d(TAG, "playWithStaicMode: reloadStaticData");
            audioTrack.play();
            return;
        }
        //3。否则,就先释放audiotrack,然后重新初始化audiotrack进行

        releaseAudioTrack();

        int state = initAudioTrackWithMode(AudioTrack.MODE_STATIC, staicBuff.length);
        if (state == AudioTrack.STATE_UNINITIALIZED) {
            Log.e(TAG, "run: state is uninit");
            return;
        }
        //4. 把pcm写入audioTrack,然后进行播放
        long startTime = System.currentTimeMillis();
        int result = audioTrack.write(staicBuff, 0, staicBuff.length);
        Log.d(TAG, "audioTrack.write staic: result=" + result+" totaltime="+ (System.currentTimeMillis() - startTime));
        audioTrack.play();
        isPlaying = true;
    }


    private void pausePlay() {
        if (audioTrack != null) {
            if (audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
                audioTrack.pause();
                audioTrack.flush();
            }
            isPlaying = false;
            Log.d(TAG, "pausePlay: isPlaying false");
        }
        if (audioTrackThread != null) {
            audioTrackThread.interrupt();
        }
    }

    private void releaseAudioTrack() {
        if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
            audioTrack.stop();
            audioTrack.release();
            isPlaying = false;
            Log.d(TAG, "pausePlay: isPlaying false");
        }
        if (audioTrackThread != null) {
            audioTrackThread.interrupt();
        }
    }

3.2 STREAM模式

1. 初始化参数   
private void initAudioTrackParams() {
        sampleRateInHz = 44100;
        channels = AudioFormat.CHANNEL_OUT_MONO;//错误的写成了CHANNEL_IN_MONO
        audioFormat = AudioFormat.ENCODING_PCM_16BIT;
        bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channels, audioFormat);

        pcmFile = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "convert.wav");//"raw.pcm"
        if (pcmFile.exists()) {
            hasPcmFile = true;
        }
    }

2. 点击进行播放
private void play() {
        releaseAudioTrack();

        int state = initAudioTrackWithMode(AudioTrack.MODE_STREAM, bufferSize);
        if (state == AudioTrack.STATE_UNINITIALIZED) {
            Log.e(TAG, "run: state is uninit");
            return;
        }

        audioTrackThread = new Thread(new Runnable() {
            @Override
            public void run() {
                FileInputStream fileInputStream = null;
                try {
                    fileInputStream = new FileInputStream(pcmFile);
                    byte[] buffer = new byte[bufferSize / 2];
                    int readCount;
                    Log.d(TAG, "run: ThreadId=" + Thread.currentThread() + " playState=" + audioTrack.getPlayState());
                    //stream模式,可以先调用play
                    audioTrack.play();
                    while (fileInputStream.available() > 0) {
                        readCount = fileInputStream.read(buffer);
                        if (readCount == AudioTrack.ERROR_BAD_VALUE || readCount == AudioTrack.ERROR_INVALID_OPERATION) {
                            continue;
                        }
                        if (audioTrack == null) {
                            return;
                        } else {
                            Log.i(TAG, "run: audioTrack.getState()" + audioTrack.getState() + " audioTrack.getPlayState()=" + audioTrack.getPlayState());
                        }
//                        audioTrack.getPlayState()
                        //一次一次的写入pcm数据到audioTrack.由于是在子线程中进行write,快速连续点击可能主线程触发了stop或者release,导致子线程write异常:IllegalStateException: Unable to retrieve AudioTrack pointer for write()
                        //所以加playstate的判断
                        if (readCount > 0 && audioTrack != null && audioTrack.getPlayState() == AudioTrack.PLAYSTATE_PLAYING && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
                            audioTrack.write(buffer, 0, readCount);
                        }
                    }

                } catch (IOException | IllegalStateException e) {
                    e.printStackTrace();
                    Log.e(TAG, "play: " + e.getMessage());
                } finally {
                    if (fileInputStream != null) {
                        try {
                            fileInputStream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    Log.d(TAG, "playWithStreamMode: end  ThreadID=" + Thread.currentThread());
                }
            }
        });
        audioTrackThread.start();
    }


    private void pausePlay() {
        if (audioTrack != null) {
            if (audioTrack.getState() > AudioTrack.STATE_UNINITIALIZED) {
                audioTrack.pause();
            }
            Log.d(TAG, "pausePlay: isPlaying false getPlayState= " + audioTrack.getPlayState());
        }
        if (audioTrackThread != null) {
            audioTrackThread.interrupt();
        }
    }

    private void releaseAudioTrack() {
        if (audioTrack != null && audioTrack.getState() == AudioTrack.STATE_INITIALIZED) {
            audioTrack.stop();
            audioTrack.release();
            Log.d(TAG, "pausePlay: isPlaying false");
        }
        if (audioTrackThread != null) {
            audioTrackThread.interrupt();
        }
    }

四、遇到的问题

纸上得来终觉浅,绝知此事要实践
本篇文章原计划昨天完成,但是在实践中遇到了不少问题,不过最终都得以解决,记录如下

1: stream模式快速点击 声音重叠,如何停止:在触发播放前先停止和释放auidoTrack,然后在进行init,在audioTrack写入数据的线程中write操作要做好audiotTrack的状态判断。具体实现见上面小节的代码

2:如何监听播放进度:AudioTrack有没有想MediaPlayer的丰富的监听回调,比如说,播放进度,播放完成回调,异常回调等。遗憾的是还真没有,针对STATIC模式的播放结束监听倒是可以借助setNotificationMarkerPosition 和 setPlaybackPositionUpdateListener来判断来判断。具体见上面小节中STATIC模式的实现

3: staic模式下有时候无法播放;音频在快速连续点击中加了isplaying的片段,如果正在playing中有触发了play,会先stop然后调用audioTrack.reloadStaticData()加载数据流,再进行播放,但是发现快速连续点击是间隔一次才会播放生效,原因还是audioTrack资源没有被正确使用,改为了先release在进行init的方式。

4: IllegalStateException: Unable to retrieve AudioTrack pointer for write():这个异常是stream模式时在主线程出发了stop或者release,而在audioTrack子线程write时抛出的异常,原因就是播放状态不对,如果已经处于Stropped状态,再进行write操作就会报这个错误,所以write时加个playstate状态的检验。具体解决实现见上面小节的代码。

五、参考

《音视频开发进阶指南》
《使用 AudioTrack 播放PCM音频》
Android 音频系统:从 AudioTrack 到 AudioFlinger

六、收获

通过对AudioTrack的学习实践,收获如下

  1. 了解了AudioTrack的在音频系统中的作用以及使用场景
  2. AudioTrack的两种模式STATIC和STREAM的区别和使用方式
  3. AudioTrack的基本使用
  4. 实践中遇到的问题以及解决方案和思路

感谢你的阅读
下一篇我们来学习分析下视频的采集即Camera,欢迎讨论交流。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352