使用 AudioTrack 播放音频轨道

01 前言

大家好,本文是 iOS/Android 音视频开发专题 的第七篇,该专题中 AVPlayer 项目代码将在 Github 进行托管,你可在微信公众号(GeekDev)后台回复 资料 获取项目地址。

在上篇文章 OpenGL ES 实现播放视频帧 中我们已经知道如何使用 GLSurfaceView 将解码后的视频渲染到屏幕上,但是,我们的播放器还不具备音频播放的功能,在本篇文章中我们将使用 AudioTrack 播放解码后的音频数据(PCM)。

本期内容:

  • PCM 介绍
  • AudioTrack API 介绍
  • 使用 MediaCodec 解码及播放音频轨道
  • 结束语

02 PCM 介绍

PCM (Pulse-code modulation 脉冲编码调制)是一种将模拟信号转为数字信号的方法。由于计算机只能识别数字信号,也就是一堆二进制序列,所以麦克风采集到的模拟信号会被模数转换器转换,生成数字信号。最常见的方式就是经过 PCM A/D 转换。

A/D 转换涉及到采样,量化和编码。

采样:由于存储空间有限,我们需要对模拟信号进行采样存储。采样就是从模拟信号进行抽样,抽样就涉及到采样频率,采样频率是每秒钟对声音样本的采样次数,采样率越高,声音质量越高,越能还原真实的声音。因此,我们一般称模拟信号是连续信号,数字信号为离散,不连续信号。

根据奈奎斯特理论,采样频率不低于音频信号最高频率的2倍,就可以无损的还原真实声音。

而由于人耳能听到的频率范围在 20Hz~20kHz,所以,为了保证声音不失真,采样频率我们一般设定为 40kHz 以上。常用的采样频率有 22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz。目前在 Android 设备中,只有 44.1kHz 是所有设备都支持的采样频率。

采样过程

量化:模拟信号经过采样成为离散信号,离散信号经过量化成为数字信号。量化是将经过采样得到的离散数据转换成二进制数的过程,量化深度表示每个采样点用多少比特表示,在计算机中音频的量化深度一般为4、8、16、32位(bit)等。

量化深度的大小影响到声音的质量,显然,位数越多,量化后的波形越接近原始波形,声音的质量越高,而需要的存储空间也越多;位数越少,声音的质量越低,需要的存储空间越少。CD音质采用的是16 bits,移动通信 8bits。

另外,WAV 文件其实就是 PCM 格式,因为播放 PCM 裸流时,我们需要知道 PCM 的采样率, 声道数, 位宽等信息,WAV 只是在文件头前添加了这部分描述信息,所以 WAV 文件可以直接播放。

WAV 文件头

PCM 是音频处理中频繁接触的格式,通常我们对音频的处理都是基于 PCM 流,如常见的音量调节, 变声, 变调等特性。

03 AudioTrack API 介绍

在 Android 中,如果你想要播放一个音频文件,我们一般优先选用 MediaPlayer,使用 MediaPlayer 时你不需要关心文件的具体格式,也不需要对文件进行解码,使用 MediaPlayer 提供的 API,我们就可以开发出一个简单的音频播放器。

AudioTrack 是播放音频的另外一种方式 「如果你感兴趣还可以了解下 SoundPool」, 并且只能用于播放 PCM 数据。

AudioTrack API 概述 :

  1. AudioTrack 初始化
/**
  * Class constructor.
  * @param streamType 流类型
  *   @link AudioManager#STREAM_VOICE_CALL, 语音通话
  *   @link AudioManager#STREAM_SYSTEM, 系统声音 如低电量
  *   @link AudioManager#STREAM_RING, 来电铃声
  *   @link AudioManager#STREAM_MUSIC, 音乐播放器
  *   @link AudioManager#STREAM_ALARM, 警告音
  *   @link AudioManager#STREAM_NOTIFICATION 通知
  *
  * @param sampleRateInHz 采样率
  *
  * @param channelConfig 声道类型
  *   @link AudioFormat#CHANNEL_OUT_MONO 单声道
  *   @link AudioFormat#CHANNEL_OUT_STEREO 双声道
  * @param audioFormat
  *  @link AudioFormat#ENCODING_PCM_16BIT,
  *  @link AudioFormat#ENCODING_PCM_8BIT,
  *  @link AudioFormat#ENCODING_PCM_FLOAT
  * @param bufferSizeInBytes 缓冲区大小
  * @param mode 模式
  *  @link #MODE_STATIC 静态模式 通过 write 将数据一次写入,适合较小文件
  *  @link #MODE_STREAM 流式模式 通过 write 分批写入,适合较大文件
  */    
public AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes, int mode)

初始化 AudioTrack 时的 bufferSizeInBytes 参数,可以通过 getMinBufferSize 计算算出合适的预估缓冲区大小,一般为 getMinBufferSize 的整数倍。

  1. 写入数据
/**
   * @param audioData 保存要播放的数据的数组
   * @param offsetInBytes 在要写入数据的audioData中以字节表示的偏移量
   * @param sizeInBytes 在偏移量之后写入audioData的字节数。
 **/
public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)
  1. 开始播放
 public void play()

如果 AudioTrack 创建时的模式为 MODE_STATIC 时,调用 play 之前必须保证 write 方法已被调用。

  1. 暂停播放
 public void pause()

暂停播放数据,尚未播放的数据不会被丢弃,再次调用 play 时将继续播放。

  1. 停止播放
public void stop()

停止播放数据,尚未播放的数据将会被丢弃。

  1. 刷新缓冲区数据
public void flush()

刷新当前排队等待播放的数据,已写入当未播放的数据将被丢弃,缓冲区将被清理。

04 MediaCodec 解码并播放音频轨道

如果我们要播放一个音频轨道,需要将音轨解码后才可以播放,之前我们一直在说如何解码视频,如果你看过 AVPlayer Demo ,你一定对如何创建视频轨道解码器很熟悉了,如果我们要解码一个音频轨道,只需要改下 mimeType 即可。创建一个音频轨道解码如下:

private void doDecoder() {
            // step 1:创建一个媒体分离器
            MediaExtractor extractor = new MediaExtractor();
            // step 2:为媒体分离器装载媒体文件路径
                    
            // 指定文件路径
            Uri videoPathUri = Uri.parse("android.resource://" + getPackageName() + "/" + R.raw.demo_video);
            
            try
             {
                extractor.setDataSource(this, videoPathUri, null);
            } 
            catch(IOException e) {
                 e.printStackTrace();
            }
                    
            // step 3:获取并选中指定类型的轨道        
            // 媒体文件中的轨道数量 (一般有视频,音频,字幕等)
            
            int trackCount = extractor.getTrackCount();        
            // mime type 指示需要分离的轨道类型 指定为音频轨道
                    
            String extractMimeType = "audio/";
            MediaFormat trackFormat = null;
            // 记录轨道索引id,MediaExtractor 读取数据之前需要指定分离的轨道索引
            int trackID = -1;
                    
            for(int i = 0; i < trackCount; i++) {
            
                trackFormat = extractor.getTrackFormat(i);
                if(trackFormat.getString(MediaFormat.KEY_MIME).startsWith(extractMimeType))
                {
                      trackID = i;        
                        break;
                }
            }
            
                    
            // 媒体文件中存在视频轨道
            // step 4:选中指定类型的轨道        
            if(trackID != -1)
                extractor.selectTrack(trackID);
               
            // step 5:根据 MediaFormat 创建解码器
              
            MediaCodec mediaCodec = null;
                
            try
             {
                  mediaCodec = MediaCodec.createDecoderByType(trackFormat.getString(MediaFormat.KEY_MIME));
                      mediaCodec.configure(trackFormat,null,null,0);
                  mediaCodec.start();
            } 
            catch(IOExceptione) {
             e.printStackTrace();
            }
                    
            while (mRuning) {
                   
            // step 6: 向解码器喂入数据
            boolean ret = feedInputBuffer(extractor,mediaCodec);
            // step 7: 从解码器吐出数据
            boolean decRet = drainOutputBuffer(mediaCodec);
            if(!ret && !decRet) break;
            
            }
                    
            // step 8: 释放资源
            // 释放分离器,释放后 extractor 将不可用
            extractor.release();
                 
            // 释放解码器
            mediaCodec.release();
            new Handler(LoopergetMainLooper()).post(new Runnable() {
                @Override
                 public void run() {
                mPlayButton.setEnabled(true);
                 mInfoTextView.setText("解码完成");
              } 
            });
}

解码音频时我们将 extractMimeType 设定为 "audio/" ,其它代码与解码视频时相同。

接着我们监听到 INFO_OUTPUT_FORMAT_CHANGED 状态时,获取该音频轨道的格式信息, MediaFormat 提供了足够的信息可以让我们初始化 AudioTrack。

    
// 从 MediaCodec 吐出解码后的音频信息
  
private boolean drainOutputBuffer(MediaCodec mediaCodec) {
        if (mediaCodec == null) return false;
        
        final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
        int outIndex =  mediaCodec.dequeueOutputBuffer(info, 0);
        
        if ((info.flags & BUFFER_FLAG_END_OF_STREAM) != 0)
        {
             mediaCodec.releaseOutputBuffer(outIndex, false);
            return false ;
        }
               
        switch (outIndex) {
            case
             INFO_OUTPUT_BUFFERS_CHANGED: return true
        case
         INFO_TRY_AGAIN_LATER: return true;
        case
         INFO_OUTPUT_FORMAT_CHANGED: {
             MediaFormat outputFormat = mediaCodec.getOutputFormat();
            int sampleRate = 44100;
         if (outputFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE))
          sampleRate = outputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                        
        int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
                  
        if
         (outputFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT))
          channelConfig = outputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == 1 ? AudioFormat.CHANNEL_OUT_MONO : AudioFormat.CHANNEL_OUT_STEREO;
                
        int audioFormat =  AudioFormat.ENCODING_PCM_16BIT;
                      
        if (outputFormat.containsKey("bit-width"))
         audioFormat = outputFormat.getInteger("bit-width") == 8 ? AudioFormat.ENCODING_PCM_8BIT : AudioFormat.ENCODING_PCM_16BIT;
        
         mBufferSize = AudioTrack.getMinBufferSize(sampleRate,channelConfig,audioFormat) * 2;
        
        mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC,sampleRate,channelConfig,audioFormat,mBufferSize,AudioTrack.MODE_STREAM);
        mAudioTrack.play();                
        return true;
         }
        
        default:
        
        {
        
        if (outIndex >= 0 && info.size > 0)
        {
            MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
        bufferInfo.presentationTimeUs = info.presentationTimeUs;
        bufferInfo.size = info.size;
        bufferInfo.flags = info.flags;
        bufferInfo.offset = info.offset;
              
        ByteBuffer outputBuffer = mediaCodec.getOutputBuffers()[outIndex];
         outputBuffer.position(bufferInfo.offset);
         outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
        
                            
        byte[] audioData = new byte[bufferInfo.size];
        outputBuffer.get(audioData);
        
        
        // 写入解码后的音频数据
        mAudioTrack.write(audioData,bufferInfo.offset,Math.min(bufferInfo.size, mBufferSize));
        
        // 释放
        mediaCodec.releaseOutputBuffer(outIndex, false);
        
                   
        return true;
         }
        
           }
}

当我们通过 INFO_OUTPUT_FORMAT_CHANGED 获取到 MediaFormat 并初始化 AudioTrack 后,就可以通过 write 方法写入解码后的音频数据。

详见: DemoAudioTrackPlayerActivity

05 结束语

关注 GeekDev 公众号获取首发内容。如果你想了解更多信息,可关注微信公众号 (GeekDev) 并回复 资料 获取。

往期内容:

iOS/Android 音视频开发专题介绍

iOS/Android 音视频概念介绍

MediaCodec/OpenMAX/StageFright 介绍

使用 MediaCodec 解码音视频

OpenGL ES for Android 世界

OpenGL ES 与 GlSurfaceView 渲染音视频

下期预告:

《 AVPlayer 添加音效 》

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

推荐阅读更多精彩内容