最近需要在Android的客戶端中使用PCM聲音播放和錄制,簡單學習了一下。有不正確的地方還請指出。
站在用戶的角度思考問題,與客戶深入溝通,找到元寶山網站設計與元寶山網站推廣的解決方案,憑借多年的經驗,讓設計與互聯網技術結合,創(chuàng)造個性化、用戶體驗好的作品,建站類型包括:網站建設、成都做網站、企業(yè)官網、英文網站、手機端網站、網站推廣、域名注冊、網絡空間、企業(yè)郵箱。業(yè)務覆蓋元寶山地區(qū)。
首先有幾個概念需要了解一下:采樣頻率、聲道數、采樣位數。
采樣頻率一般是sample rate, 代表的是數字化音頻時每秒采樣的次數。常見的有44.1KHz(CD品質)、48KHz等。
這個很好理解,單聲道Mono就是聲音從一個方向傳出來;雙聲道Stereo也叫立體聲,聲音是從兩個方向傳來。通常的流行音樂中,仔細聽能發(fā)現每個聲道可能側重不同的樂曲聲部,比如左聲道吉他,右聲道鋼琴,人聲似乎兩個聲道都有,聽起來就像站在中間一樣。(這里沒有考證,隨便舉例)
每一個采樣都是一個數據點,采樣位數是指這個數據點使用了幾位來記錄。AudioTrack類只支持8位和16位的PCM音頻。8位就是2的8次方,即256個值;而16位則是2的16次方,有65536個值。
這個在音頻的編解碼中還是比較常用的。在PCM格式中,1秒鐘音頻的數據大小是SampleRate×Channel×Bit/8,單位是byte字節(jié)。由于PCM本身沒有音頻幀的概念,所以通過這個公式就能計算出任意時長音頻的大小,或者得到任意大小音頻的時長。如果規(guī)定1個音頻幀是“每個聲道256個采樣”,雙聲道下就是512個采樣,那么1幀的數據量就是256×Channel×Bit/8,同理可以推斷出1秒鐘有多少音頻幀等等。音頻幀的概念在各種編解碼中各有不同,但計算公式大同小異,這里不展開。
Android中音頻的播放使用的是AudioTrack類,具體用法非常簡單。
首先設置buffer大小。AudioTrack播放時需要先寫入buffer,如果這個buffer沒有寫滿,那么這部分是不會播放的。所以buffer不能設置太小,這樣會導致播放不連貫;而buffer也不能設置太小,這樣不間斷寫入會消耗許多CPU資源。AudioTrack自帶了getMinBufferSize方法可以給出一個最小buffer,一般用這個值就可以。getMinBufferSize方法三個參數分別是sample rate、channel和bit。
設置完buffer size就可以實例化一個AudioTrack。其中第一個參數streamType是指不同的音頻流類型,包括STREAM_MUSIC、STREAM_ALARM、STREAM_VOICE_CALL、STREAM_RING等,是Android對不同音頻的分類。中間三個參數很好理解,第四個是buffer size,剛剛計算出來了。最后一個參數mode有兩種:MODE_STREAM和MODE_STATIC。前者是以流形式播放,后者則是一次性全部寫入然后播放。
調用實例的play()方法就可以開始播放了。不過播放得要有數據吧?要填寫數據就要用到write()方法。write方法中第一個參數是一個byte[]類型,是要寫入的數據源,可以是從文件流中讀取出來的;第二個參數offset是初始位移,即從source的哪個位置開始;第三個參數則是輸入長度。
當write方法寫滿一個AudioTrack的buffer時,就會有聲音播放出來了。
當播放完成后記得要把AudioTrack停止并釋放。
Android 音頻焦點(Audio Focus)
引子
說 Audio Focus 前先說個很簡單需求:來電時暫停正在播放的音樂,電話結束時恢復播放。
音頻焦點
問題的解決方法就是:請求系統(tǒng)的音頻焦點(Request the Audio Focus)。
官方文檔指出Android 在處理音頻播放是分了多個“音頻流”的,如音樂流、音效流、電話聲音流等,使控制音量時可以互不干涉。多數情況下我們播放音樂都是使用 STREAM_MUSIC 音頻流。
另外,系統(tǒng)中可能會有多個應用程序會播放音頻,所以需要考慮他們之間該如何協(xié)調,為了避免同時播放音樂,Android 系統(tǒng)使用音頻焦點來進行統(tǒng)一管理,即只有獲得了音頻焦點的應用程序才可以播放音樂。
那么,播放音頻應該這樣來做:
獲取音頻焦點 requestAudioFocus
獲取成功后,開始播放音頻
處理音頻焦點的丟失和“DUCK”
播放完畢后取消焦點
如此便可以完美的解決引子里的需求。
一個簡單的示例
MusicService.java
public class MusicService extends Service {
private AudioManager mAm;
private boolean isPlaymusic;
private String url;
private MediaPlayer mediaPlayer;
@Override
public void onCreate() {
? ? super.onCreate();
? ? mAm = (AudioManager) getSystemService(AUDIO_SERVICE);
}
@Override
public void onStart(Intent intent, int startId) {
? ? if (intent != null) {
? ? ? ? Bundle bundle = intent.getExtras();
? ? ? ? if (bundle != null) {
? ? ? ? ? ? isPlaymusic = bundle.getBoolean("isPlay", true);
? ? ? ? ? ? url = bundle.getString("url");
? ? ? ? ? ? if (isPlaymusic)
? ? ? ? ? ? ? ? play();
? ? ? ? ? ? else
? ? ? ? ? ? ? ? stop();
? ? ? ? }
? ? }
}
OnAudioFocusChangeListener afChangeListener = new OnAudioFocusChangeListener() {
public void onAudioFocusChange(int focusChange) {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
// Pause playback
pause();
} else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
// Resume playback
resume();
} else if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
// mAm.unregisterMediaButtonEventReceiver(RemoteControlReceiver);
mAm.abandonAudioFocus(afChangeListener);
// Stop playback
stop();
}
}
};
private boolean requestFocus() {
// Request audio focus for playback
int result = mAm.requestAudioFocus(afChangeListener,
// Use the music stream.
AudioManager.STREAM_MUSIC,
// Request permanent focus.
AudioManager.AUDIOFOCUS_GAIN);
return result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
}
private void resume() {
if (mediaPlayer != null) {
mediaPlayer.start();
}
}
private void pause() {
if (mediaPlayer != null mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
OnCompletionListener completionListener = new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer player) {
if(!player.isLooping()){
mAm.abandonAudioFocus(afChangeListener);
}
}
};
private void play() {
if (requestFocus()) {
if (mediaPlayer == null) {
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setDataSource(url);
mediaPlayer.prepare();
mediaPlayer.setOnCompletionListener(completionListener);
} catch (IOException e) {
e.printStackTrace();
}
}
if (!mediaPlayer.isPlaying()) {
mediaPlayer.start();
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (mediaPlayer != null)
mediaPlayer.release();
}
private void stop() {
if (mediaPlayer != null) {
mediaPlayer.stop();
}
}
@Override
public IBinder onBind(Intent arg0) {
// TODO Auto-generated method stub
return null;
}
}
經模擬器測試,當來電時音頻焦點會給到鈴聲流,并打出日志:
I/AudioService(1235):? AudioFocus? requestAudioFocus() from AudioFocus_For_Phone_Ring_And_Calls
此時MusicService中的afChangeListener會得到AUDIOFOCUS_LOSS_TRANSIENT,于是會暫停播放音頻。
當通話結束或者掛掉電話,afChangeListener會得到AUDIOFOCUS_GAIN,于是恢復播放音頻。
注意:
播放完畢一定要禁止掉請求的音頻焦點abandonAudioFocus(afChangeListener),否則,如果播放完畢后的某個時段剛好有個通話結束,并且此時沒有其他的應用占用了焦點,系統(tǒng)會重新通知服務里的afChangeListener,導致音頻再次的播放。
如果丟失的短暫音頻焦點允許DUCK狀態(tài)AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK,在這種情況下,應用程序降低音量繼續(xù)播放,不需要暫停。再次獲取后,恢復原來的音量。
最近項目中需要實現手機采集聲音頻率實現設備律動的效果,整理了下Android與聲音相關的知識。
根據聲音振幅、頻率獲取顏色值,通過藍牙m(xù)esh發(fā)送指令給燈改變其顏色值。
Android聲音采集相關Api
快速傅里葉變換公式
Mesh網發(fā)送rgb值相關指令
人主觀感覺聲音的大?。ㄒ袅浚?,振幅與人離聲源的距離決定,振幅越大,離聲源的距離越小,響度越大。
LP= 20×lgP/P0
LP:聲壓級(db)
P:聲壓(Pa)
P0:基準聲壓:2*10-5Pa,該值是對800HZ聲音人耳剛能聽到的最低聲壓。
聲音的高低,由頻率決定,頻率越高,音調越高。
頻率是每秒經過一給定點的聲波數量,單位赫茲(Hz)
人耳能聽到20~20kHz的聲音。
音品,波形決定聲音的音色。
MediaRecorder:基于文件錄音,已集成錄音、編碼、壓縮
把模擬信號數字化的過程
采樣頻率越高,紅色間隔越密集,記錄音頻所用數據量越大,音頻質量越高。
采樣定理(奈奎斯特理論):當采樣頻率大于信號中最高頻率的2倍時,采樣后的數字信號完整地保留原始信號中的信息。人耳能聽到20~20kHz的聲音,為了保證聲音不失真,采樣頻率應在40kHz以上。
目前44100Hz是唯一可以保證兼容所有Android手機的采樣率。
指將模擬信號分成幾個等級,量化精度越高,聲音質量越好,單位Bit。
CD標準量化精度16Bit,DVD標準量化精度24Bit。
16Bit可以保證兼容所有Android手機。
音頻采集、播放可以疊加,可以同時從多個音頻源采集聲音,例如:單聲道/雙聲道。
即采樣時間,例如20ms一幀代表20ms為單位的數據量為一幀音頻。
一幀音頻幀大小 = 采樣率 x 位寬 x 采樣時間 x 通道數
例:采樣率8000,位寬8,通道2,采樣間隔20ms
(8000 * 8/8 *2)/ (1000/20 ) = 320Byte //1字節(jié) = 8 bits
對audioData進行快速傅里葉變化,時域-頻域的變化,可以將信號的頻譜提取出來。
傅立葉變換就是多個正余弦波疊加可以用來近似任何一個原始的周期函數,它實質是是頻域函數和時域函數的轉換。
Visualizer:檢索當前正在播放的音頻,對其進行編碼
以下基于AudioRecord采集的音頻數據后進行快速傅里葉變換得到頻率值
向各位推薦百度網盤的會員專享功能“音頻倍速”,它可以五種倍速模式隨意轉換,滿足各種用戶的需求。
步驟一:打開百度網盤APP,點擊“文件”
?步驟二:找到需要播放的音頻文件,點擊它,進入播放頁面。
?步驟三:點擊“倍速”,即可調整倍速。
幀,是視頻的一個基本概念,表示一張畫面,如上面的翻頁動畫書中的一頁,就是一幀。一個視頻就是由許許多多幀組成的。
幀率,即單位時間內幀的數量,單位為:幀/秒 或fps(frames per second)。一秒內包含多少張圖片,圖片越多,畫面越順滑,過渡越自然。 幀率的一般以下幾個典型值:
24/25 fps:1秒 24/25 幀,一般的電影幀率。
30/60 fps:1秒 30/60 幀,游戲的幀率,30幀可以接受,60幀會感覺更加流暢逼真。
85 fps以上人眼基本無法察覺出來了,所以更高的幀率在視頻里沒有太大意義。
這里我們只講常用到的兩種色彩空間。
RGB的顏色模式應該是我們最熟悉的一種,在現在的電子設備中應用廣泛。通過R G B三種基礎色,可以混合出所有的顏色。
這里著重講一下YUV,這種色彩空間并不是我們熟悉的。這是一種亮度與色度分離的色彩格式。
早期的電視都是黑白的,即只有亮度值,即Y。有了彩色電視以后,加入了UV兩種色度,形成現在的YUV,也叫YCbCr。
Y:亮度,就是灰度值。除了表示亮度信號外,還含有較多的綠色通道量。
U:藍色通道與亮度的差值。
V:紅色通道與亮度的差值。
音頻數據的承載方式最常用的是 脈沖編碼調制 ,即 PCM 。
在自然界中,聲音是連續(xù)不斷的,是一種模擬信號,那怎樣才能把聲音保存下來呢?那就是把聲音數字化,即轉換為數字信號。
我們知道聲音是一種波,有自己的振幅和頻率,那么要保存聲音,就要保存聲音在各個時間點上的振幅。
而數字信號并不能連續(xù)保存所有時間點的振幅,事實上,并不需要保存連續(xù)的信號,就可以還原到人耳可接受的聲音。
根據奈奎斯特采樣定理:為了不失真地恢復模擬信號,采樣頻率應該不小于模擬信號頻譜中最高頻率的2倍。
根據以上分析,PCM的采集步驟分為以下步驟:
采樣率,即采樣的頻率。
上面提到,采樣率要大于原聲波頻率的2倍,人耳能聽到的最高頻率為20kHz,所以為了滿足人耳的聽覺要求,采樣率至少為40kHz,通常為44.1kHz,更高的通常為48kHz。
采樣位數,涉及到上面提到的振幅量化。波形振幅在模擬信號上也是連續(xù)的樣本值,而在數字信號中,信號一般是不連續(xù)的,所以模擬信號量化以后,只能取一個近似的整數值,為了記錄這些振幅值,采樣器會采用一個固定的位數來記錄這些振幅值,通常有8位、16位、32位。
位數越多,記錄的值越準確,還原度越高。
最后就是編碼了。由于數字信號是由0,1組成的,因此,需要將幅度值轉換為一系列0和1進行存儲,也就是編碼,最后得到的數據就是數字信號:一串0和1組成的數據。
整個過程如下:
聲道數,是指支持能不同發(fā)聲(注意是不同聲音)的音響的個數。 單聲道:1個聲道
雙聲道:2個聲道
立體聲道:默認為2個聲道
立體聲道(4聲道):4個聲道
碼率,是指一個數據流中每秒鐘能通過的信息量,單位bps(bit per second)
碼率 = 采樣率 * 采樣位數 * 聲道數
這里的編碼和上面音頻中提到的編碼不是同個概念,而是指壓縮編碼。
我們知道,在計算機的世界中,一切都是0和1組成的,音頻和視頻數據也不例外。由于音視頻的數據量龐大,如果按照裸流數據存儲的話,那將需要耗費非常大的存儲空間,也不利于傳送。而音視頻中,其實包含了大量0和1的重復數據,因此可以通過一定的算法來壓縮這些0和1的數據。
特別在視頻中,由于畫面是逐漸過渡的,因此整個視頻中,包含了大量畫面/像素的重復,這正好提供了非常大的壓縮空間。
因此,編碼可以大大減小音視頻數據的大小,讓音視頻更容易存儲和傳送。
視頻編碼格式有很多,比如H26x系列和MPEG系列的編碼,這些編碼格式都是為了適應時代發(fā)展而出現的。
其中,H26x(1/2/3/4/5)系列由ITU(International Telecommunication Union)國際電傳視訊聯盟主導
MPEG(1/2/3/4)系列由MPEG(Moving Picture Experts Group, ISO旗下的組織)主導。
當然,他們也有聯合制定的編碼標準,那就是現在主流的編碼格式H264,當然還有下一代更先進的壓縮編碼標準H265。
H264是目前最主流的視頻編碼標準,所以我們后續(xù)的文章中主要以該編碼格式為基準。
H264由ITU和MPEG共同定制,屬于MPEG-4第十部分內容。
我們已經知道,視頻是由一幀一幀畫面構成的,但是在視頻的數據中,并不是真正按照一幀一幀原始數據保存下來的(如果這樣,壓縮編碼就沒有意義了)。
H264會根據一段時間內,畫面的變化情況,選取一幀畫面作為完整編碼,下一幀只記錄與上一幀完整數據的差別,是一個動態(tài)壓縮的過程。
在H264中,三種類型的幀數據分別為
I幀:幀內編碼幀。就是一個完整幀。
P幀:前向預測編碼幀。是一個非完整幀,通過參考前面的I幀或P幀生成。
B幀:雙向預測內插編碼幀。參考前后圖像幀編碼生成。B幀依賴其前最近的一個I幀或P幀及其后最近的一個P幀。
全稱:Group of picture。指一組變化不大的視頻幀。
GOP的第一幀成為關鍵幀:IDR
IDR都是I幀,可以防止一幀解碼出錯,導致后面所有幀解碼出錯的問題。當解碼器在解碼到IDR的時候,會將之前的參考幀清空,重新開始一個新的序列,這樣,即便前面一幀解碼出現重大錯誤,也不會蔓延到后面的數據中。
DTS全稱:Decoding Time Stamp。標示讀入內存中數據流在什么時候開始送入解碼器中進行解碼。也就是解碼順序的時間戳。
PTS全稱:Presentation Time Stamp。用于標示解碼后的視頻幀什么時候被顯示出來。
前面我們介紹了RGB和YUV兩種圖像色彩空間。H264采用的是YUV。
YUV存儲方式分為兩大類:planar 和 packed。
planar如下:
packed如下:
上面說過,由于人眼對色度敏感度低,所以可以通過省略一些色度信息,即亮度共用一些色度信息,進而節(jié)省存儲空間。因此,planar又區(qū)分了以下幾種格式:YUV444、 YUV422、YUV420。
YUV 4:4:4采樣,每一個Y對應一組UV分量。
YUV 4:2:2采樣,每兩個Y共用一組UV分量。
YUV 4:2:0采樣,每四個Y共用一組UV分量。
其中,最常用的就是YUV420。
YUV420屬于planar存儲方式,但是又分兩種類型:
YUV420P:三平面存儲。數據組成為YYYYYYYYUUVV(如I420)或YYYYYYYYVVUU(如YV12)。
YUV420SP:兩平面存儲。分為兩種類型YYYYYYYYUVUV(如NV12)或YYYYYYYYVUVU(如NV21)
原始的PCM音頻數據也是非常大的數據量,因此也需要對其進行壓縮編碼。
和視頻編碼一樣,音頻也有許多的編碼格式,如:WAV、MP3、WMA、APE、FLAC等等,音樂發(fā)燒友應該對這些格式非常熟悉,特別是后兩種無損壓縮格式。
但是,我們今天的主角不是他們,而是另外一個叫AAC的壓縮格式。
AAC是新一代的音頻有損壓縮技術,一種高壓縮比的音頻壓縮算法。在MP4視頻中的音頻數據,大多數時候都是采用AAC壓縮格式。
AAC格式主要分為兩種:ADIF、ADTS。
ADIF:Audio Data Interchange Format。音頻數據交換格式。這種格式的特征是可以確定的找到這個音頻數據的開始,不需進行在音頻數據流中間開始的解碼,即它的解碼必須在明確定義的開始處進行。這種格式常用在磁盤文件中。
ADTS:Audio Data Transport Stream。音頻數據傳輸流。這種格式的特征是它是一個有同步字的比特流,解碼可以在這個流中任何位置開始。它的特征類似于mp3數據流格式。
ADIF數據格式:
ADTS 一幀 數據格式(中間部分,左右省略號為前后數據幀):
AAC內部結構也不再贅述,可以參考AAC 文件解析及解碼流程
細心的讀者可能已經發(fā)現,前面我們介紹的各種音視頻的編碼格式,沒有一種是我們平時使用到的視頻格式,比如:mp4、rmvb、avi、mkv、mov...
沒錯,這些我們熟悉的視頻格式,其實是包裹了音視頻編碼數據的容器,用來把以特定編碼標準編碼的視頻流和音頻流混在一起,成為一個文件。
例如:mp4支持H264、H265等視頻編碼和AAC、MP3等音頻編碼。
我們在一些播放器中會看到,有硬解碼和軟解碼兩種播放形式給我們選擇,但是我們大部分時候并不能感覺出他們的區(qū)別,對于普通用戶來說,只要能播放就行了。
那么他們內部究竟有什么區(qū)別呢?
在手機或者PC上,都會有CPU、GPU或者解碼器等硬件。通常,我們的計算都是在CPU上進行的,也就是我們軟件的執(zhí)行芯片,而GPU主要負責畫面的顯示(是一種硬件加速)。
所謂軟解碼,就是指利用CPU的計算能力來解碼,通常如果CPU的能力不是很強的時候,一則解碼速度會比較慢,二則手機可能出現發(fā)熱現象。但是,由于使用統(tǒng)一的算法,兼容性會很好。
硬解碼,指的是利用手機上專門的解碼芯片來加速解碼。通常硬解碼的解碼速度會快很多,但是由于硬解碼由各個廠家實現,質量參差不齊,非常容易出現兼容性問題。
MediaCodec 是Android 4.1(api 16)版本引入的編解碼接口,是所有想在Android上開發(fā)音視頻的開發(fā)人員繞不開的坑。
由于Android碎片化嚴重,雖然經過多年的發(fā)展,Android硬解已經有了很大改觀,但實際上各個廠家實現不同, 還是會有一些意想不到的坑。
相對于FFmpeg,Android原生硬解碼還是相對容易入門一些,所以接下來,我將會從MediaCodec入手,講解如何實現視頻的編解碼,以及引入OpenGL實現對視頻的編輯,最后才引入FFmpeg來實現軟解,算是一個比較常規(guī)的音視頻開發(fā)入門流程吧。
播放音頻用到MediaPlayer類,具體用法如下:
我們寫一個簡單的例子,播放手機存儲的根目錄下motto.mp3文件。定義三個按鈕play、pause、stop來控制播放。
另外,本范例涉及到SD卡的讀取,還要在在Manifest.xml注冊寫SD卡的權限。