使用 AudioTrack 播放音頻軌道

01 前言

大家好成榜,本文是 iOS/Android 音視頻開發(fā)專題 的第七篇苛萎,該專題中 AVPlayer 項(xiàng)目代碼將在 Github 進(jìn)行托管,你可在微信公眾號(GeekDev)后臺回復(fù) 資料 獲取項(xiàng)目地址偏化。

在上篇文章 OpenGL ES 實(shí)現(xiàn)播放視頻幀 中我們已經(jīng)知道如何使用 GLSurfaceView 將解碼后的視頻渲染到屏幕上,但是,我們的播放器還不具備音頻播放的功能,在本篇文章中我們將使用 AudioTrack 播放解碼后的音頻數(shù)據(jù)(PCM)巡李。

本期內(nèi)容:

  • PCM 介紹
  • AudioTrack API 介紹
  • 使用 MediaCodec 解碼及播放音頻軌道
  • 結(jié)束語

02 PCM 介紹

PCM (Pulse-code modulation 脈沖編碼調(diào)制)是一種將模擬信號轉(zhuǎn)為數(shù)字信號的方法。由于計(jì)算機(jī)只能識別數(shù)字信號扶认,也就是一堆二進(jìn)制序列侨拦,所以麥克風(fēng)采集到的模擬信號會被模數(shù)轉(zhuǎn)換器轉(zhuǎn)換,生成數(shù)字信號辐宾。最常見的方式就是經(jīng)過 PCM A/D 轉(zhuǎn)換狱从。

A/D 轉(zhuǎn)換涉及到采樣膨蛮,量化和編碼。

采樣:由于存儲空間有限季研,我們需要對模擬信號進(jìn)行采樣存儲敞葛。采樣就是從模擬信號進(jìn)行抽樣,抽樣就涉及到采樣頻率与涡,采樣頻率是每秒鐘對聲音樣本的采樣次數(shù)惹谐,采樣率越高,聲音質(zhì)量越高递沪,越能還原真實(shí)的聲音豺鼻。因此,我們一般稱模擬信號是連續(xù)信號款慨,數(shù)字信號為離散,不連續(xù)信號谬莹。

根據(jù)奈奎斯特理論檩奠,采樣頻率不低于音頻信號最高頻率的2倍,就可以無損的還原真實(shí)聲音附帽。

而由于人耳能聽到的頻率范圍在 20Hz~20kHz埠戳,所以,為了保證聲音不失真蕉扮,采樣頻率我們一般設(shè)定為 40kHz 以上整胃。常用的采樣頻率有 22.05kHz、16kHz喳钟、37.8kHz屁使、44.1kHz、48kHz奔则。目前在 Android 設(shè)備中蛮寂,只有 44.1kHz 是所有設(shè)備都支持的采樣頻率。

采樣過程

量化:模擬信號經(jīng)過采樣成為離散信號易茬,離散信號經(jīng)過量化成為數(shù)字信號酬蹋。量化是將經(jīng)過采樣得到的離散數(shù)據(jù)轉(zhuǎn)換成二進(jìn)制數(shù)的過程,量化深度表示每個(gè)采樣點(diǎn)用多少比特表示抽莱,在計(jì)算機(jī)中音頻的量化深度一般為4范抓、8、16食铐、32位(bit)等匕垫。

量化深度的大小影響到聲音的質(zhì)量,顯然璃岳,位數(shù)越多年缎,量化后的波形越接近原始波形悔捶,聲音的質(zhì)量越高,而需要的存儲空間也越多单芜;位數(shù)越少蜕该,聲音的質(zhì)量越低,需要的存儲空間越少洲鸠。CD音質(zhì)采用的是16 bits堂淡,移動通信 8bits。

另外扒腕,WAV 文件其實(shí)就是 PCM 格式绢淀,因?yàn)椴シ?PCM 裸流時(shí),我們需要知道 PCM 的采樣率, 聲道數(shù), 位寬等信息瘾腰,WAV 只是在文件頭前添加了這部分描述信息皆的,所以 WAV 文件可以直接播放。

WAV 文件頭

PCM 是音頻處理中頻繁接觸的格式蹋盆,通常我們對音頻的處理都是基于 PCM 流费薄,如常見的音量調(diào)節(jié), 變聲, 變調(diào)等特性。

03 AudioTrack API 介紹

在 Android 中栖雾,如果你想要播放一個(gè)音頻文件楞抡,我們一般優(yōu)先選用 MediaPlayer,使用 MediaPlayer 時(shí)你不需要關(guān)心文件的具體格式析藕,也不需要對文件進(jìn)行解碼召廷,使用 MediaPlayer 提供的 API,我們就可以開發(fā)出一個(gè)簡單的音頻播放器账胧。

AudioTrack 是播放音頻的另外一種方式 「如果你感興趣還可以了解下 SoundPool」竞慢, 并且只能用于播放 PCM 數(shù)據(jù)。

AudioTrack API 概述 :

  1. AudioTrack 初始化
/**
  * Class constructor.
  * @param streamType 流類型
  *   @link AudioManager#STREAM_VOICE_CALL, 語音通話
  *   @link AudioManager#STREAM_SYSTEM, 系統(tǒng)聲音 如低電量
  *   @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 緩沖區(qū)大小
  * @param mode 模式
  *  @link #MODE_STATIC 靜態(tài)模式 通過 write 將數(shù)據(jù)一次寫入找爱,適合較小文件
  *  @link #MODE_STREAM 流式模式 通過 write 分批寫入梗顺,適合較大文件
  */    
public AudioTrack (int streamType, int sampleRateInHz, int channelConfig, int audioFormat,int bufferSizeInBytes, int mode)

初始化 AudioTrack 時(shí)的 bufferSizeInBytes 參數(shù),可以通過 getMinBufferSize 計(jì)算算出合適的預(yù)估緩沖區(qū)大小车摄,一般為 getMinBufferSize 的整數(shù)倍寺谤。

  1. 寫入數(shù)據(jù)
/**
   * @param audioData 保存要播放的數(shù)據(jù)的數(shù)組
   * @param offsetInBytes 在要寫入數(shù)據(jù)的audioData中以字節(jié)表示的偏移量
   * @param sizeInBytes 在偏移量之后寫入audioData的字節(jié)數(shù)。
 **/
public int write(@NonNull byte[] audioData, int offsetInBytes, int sizeInBytes)
  1. 開始播放
 public void play()

如果 AudioTrack 創(chuàng)建時(shí)的模式為 MODE_STATIC 時(shí)吮播,調(diào)用 play 之前必須保證 write 方法已被調(diào)用变屁。

  1. 暫停播放
 public void pause()

暫停播放數(shù)據(jù),尚未播放的數(shù)據(jù)不會被丟棄意狠,再次調(diào)用 play 時(shí)將繼續(xù)播放粟关。

  1. 停止播放
public void stop()

停止播放數(shù)據(jù),尚未播放的數(shù)據(jù)將會被丟棄环戈。

  1. 刷新緩沖區(qū)數(shù)據(jù)
public void flush()

刷新當(dāng)前排隊(duì)等待播放的數(shù)據(jù)闷板,已寫入當(dāng)未播放的數(shù)據(jù)將被丟棄澎灸,緩沖區(qū)將被清理。

04 MediaCodec 解碼并播放音頻軌道

如果我們要播放一個(gè)音頻軌道遮晚,需要將音軌解碼后才可以播放性昭,之前我們一直在說如何解碼視頻,如果你看過 AVPlayer Demo 县遣,你一定對如何創(chuàng)建視頻軌道解碼器很熟悉了糜颠,如果我們要解碼一個(gè)音頻軌道,只需要改下 mimeType 即可萧求。創(chuàng)建一個(gè)音頻軌道解碼如下:

private void doDecoder() {
            // step 1:創(chuàng)建一個(gè)媒體分離器
            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:獲取并選中指定類型的軌道        
            // 媒體文件中的軌道數(shù)量 (一般有視頻其兴,音頻,字幕等)
            
            int trackCount = extractor.getTrackCount();        
            // mime type 指示需要分離的軌道類型 指定為音頻軌道
                    
            String extractMimeType = "audio/";
            MediaFormat trackFormat = null;
            // 記錄軌道索引id夸政,MediaExtractor 讀取數(shù)據(jù)之前需要指定分離的軌道索引
            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:根據(jù) MediaFormat 創(chuàng)建解碼器
              
            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: 向解碼器喂入數(shù)據(jù)
            boolean ret = feedInputBuffer(extractor,mediaCodec);
            // step 7: 從解碼器吐出數(shù)據(jù)
            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("解碼完成");
              } 
            });
}

解碼音頻時(shí)我們將 extractMimeType 設(shè)定為 "audio/" ,其它代碼與解碼視頻時(shí)相同守问。

接著我們監(jiān)聽到 INFO_OUTPUT_FORMAT_CHANGED 狀態(tài)時(shí)法绵,獲取該音頻軌道的格式信息, 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);
        
        
        // 寫入解碼后的音頻數(shù)據(jù)
        mAudioTrack.write(audioData,bufferInfo.offset,Math.min(bufferInfo.size, mBufferSize));
        
        // 釋放
        mediaCodec.releaseOutputBuffer(outIndex, false);
        
                   
        return true;
         }
        
           }
}

當(dāng)我們通過 INFO_OUTPUT_FORMAT_CHANGED 獲取到 MediaFormat 并初始化 AudioTrack 后,就可以通過 write 方法寫入解碼后的音頻數(shù)據(jù)盐茎。

詳見: DemoAudioTrackPlayerActivity

05 結(jié)束語

關(guān)注 GeekDev 公眾號獲取首發(fā)內(nèi)容兴垦。如果你想了解更多信息,可關(guān)注微信公眾號 (GeekDev) 并回復(fù) 資料 獲取字柠。

往期內(nèi)容:

iOS/Android 音視頻開發(fā)專題介紹

iOS/Android 音視頻概念介紹

MediaCodec/OpenMAX/StageFright 介紹

使用 MediaCodec 解碼音視頻

OpenGL ES for Android 世界

OpenGL ES 與 GlSurfaceView 渲染音視頻

下期預(yù)告:

《 AVPlayer 添加音效 》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末探越,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子窑业,更是在濱河造成了極大的恐慌钦幔,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件常柄,死亡現(xiàn)場離奇詭異鲤氢,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)西潘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進(jìn)店門卷玉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人喷市,你說我怎么就攤上這事相种。” “怎么了品姓?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵寝并,是天一觀的道長箫措。 經(jīng)常有香客問我,道長衬潦,這世上最難降的妖魔是什么斤蔓? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮别渔,結(jié)果婚禮上附迷,老公的妹妹穿的比我還像新娘。我一直安慰自己哎媚,他們只是感情好喇伯,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著拨与,像睡著了一般稻据。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上买喧,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天捻悯,我揣著相機(jī)與錄音,去河邊找鬼淤毛。 笑死今缚,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的低淡。 我是一名探鬼主播姓言,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蔗蹋!你這毒婦竟也來了何荚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤猪杭,失蹤者是張志新(化名)和其女友劉穎餐塘,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體皂吮,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡戒傻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涮较。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片稠鼻。...
    茶點(diǎn)故事閱讀 40,438評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖狂票,靈堂內(nèi)的尸體忽然破棺而出候齿,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布慌盯,位于F島的核電站周霉,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏亚皂。R本人自食惡果不足惜俱箱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望灭必。 院中可真熱鬧狞谱,春花似錦、人聲如沸禁漓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽播歼。三九已至伶跷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間秘狞,已是汗流浹背叭莫。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烁试,地道東北人雇初。 一個(gè)月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像减响,于是被迫代替她去往敵國和親抵皱。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評論 2 359

推薦閱讀更多精彩內(nèi)容