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 文件可以直接播放。
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 概述 :
- 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ù)倍寺谤。
- 寫入數(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)
- 開始播放
public void play()
如果 AudioTrack 創(chuàng)建時(shí)的模式為 MODE_STATIC 時(shí)吮播,調(diào)用 play 之前必須保證 write 方法已被調(diào)用变屁。
- 暫停播放
public void pause()
暫停播放數(shù)據(jù),尚未播放的數(shù)據(jù)不會被丟棄意狠,再次調(diào)用 play 時(shí)將繼續(xù)播放粟关。
- 停止播放
public void stop()
停止播放數(shù)據(jù),尚未播放的數(shù)據(jù)將會被丟棄环戈。
- 刷新緩沖區(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)容:
MediaCodec/OpenMAX/StageFright 介紹
OpenGL ES 與 GlSurfaceView 渲染音視頻
下期預(yù)告:
《 AVPlayer 添加音效 》