使用 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 是所有设备都支持的采样频率。

使用 AudioTrack 播放音频轨道_第1张图片
采样过程

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

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

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

使用 AudioTrack 播放音频轨道_第2张图片
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 添加音效 》

使用 AudioTrack 播放音频轨道_第3张图片
GeekDev

你可能感兴趣的:(使用 AudioTrack 播放音频轨道)