基於Android的音樂播放器的設計與實現

南方吳彥祖_藍斯發表於2021-09-26

本文基於Android音訊API提供的四個層面的音訊API,說說Android系統的音訊架構。

下面先上這張經典的Android系統架構圖

基於Android的音樂播放器的設計與實現

從圖上看Andorid整個系統層面從下到上分以下四層:

  1. Linux Kernel
  2. 硬體適配層
  3. Framework層(可分為Java層與C++層)
  4. APP層

我們上面介紹的四個層面的音訊API實現均在Framework層,其他各層音訊相關有哪些功能?當我們呼叫某一API時最終是怎麼驅動硬體工作的呢?下面我們先看看系統各層音訊相關模組及功能。

1. 各層音訊模組

基於Android的音樂播放器的設計與實現

1.1 Java層

Java層提供了 android.media API 與音訊硬體進行互動。在內部,此程式碼會呼叫相應的 JNI 類,以訪問與音訊硬體互動的原生程式碼。

  • 原始碼目錄:frameworks/base/media/java/android/media/

  • AudioManager:音訊管理器,包括音量管理、AudioFocus管理、音訊裝置管理、模式管理;

  • 錄音:AudioRecord、MediaRecorder;

  • 播放:AudioTrack、MedaiPlayer、SoundPool、ToneGenerator;

  • 編解碼:MediaCodec,音影片資料 編解碼介面。

1.2 JNI層

與 android.media 關聯的 JNI 程式碼可呼叫較低階別的原生程式碼,以訪問音訊硬體。JNI 位於 frameworks/base/core/jni/ 和 frameworks/base/media/jni 中。

在這裡可以呼叫我們上篇文章介紹的AAudio和OpenSLES介面。

1.3 Native framework 原生框架層

不管是Java層還是JNI層都只是對外提供的介面,真正的實現在原生框架層。原生框架可提供相當於 android.media 軟體包的原生軟體包,從而呼叫 Binder IPC 代理以訪問媒體伺服器的特定於音訊的服務。原生框架程式碼位於  frameworks/av/media/libmedia 或 frameworks/av/media/libaudioclient中(不同版本,位置有所改變)。

1.4 Binder IPC

Binder IPC 代理用於促進跨越程式邊界的通訊。代理位於  frameworks/av/media/libmediaframeworks/av/media/libaudioclient中,並以字母“I”開頭。

1.5 Audio Server

Audio系統在Android中負責音訊方面的資料流傳輸和控制功能,也負責音訊裝置的管理。這個部分作為Android的Audio系統的輸入/輸出層次,一般負責播放PCM聲音輸出和從外部獲取PCM聲音,以及管理聲音裝置和設定(注意:解碼功能不在這裡實現,在android系統裡音訊影片的解碼是opencore或stagefright完成的,在解碼之後才呼叫音訊系統的介面,建立音訊流並播放)。Audio服務在Android N(7.0)之前存在於mediaserver中,Android N開始以audioserver形式存在,這些音訊服務是與HAL 實現進行互動的實際程式碼。媒體伺服器位於  frameworks/av/services/audioflinger 和 frameworks/av/services/audiopolicy中。

Audio服務包含AudioFlinger 和AudioPolicyService:

  • AudioFlinger:主要負責音訊流裝置的管理以及音訊流資料的處理傳輸,⾳量計算,重取樣、混⾳、⾳效等。

  • AudioPolicyService:主要負責⾳頻策略相關,⾳量調節⽣效,裝置選擇,⾳頻通路選擇等。

1.6 HAL層

HAL 定義了由音訊服務呼叫且手機必須實現以確保音訊硬體功能正常執行的標準介面。音訊 HAL 介面位於  hardware/libhardware/include/hardware 中。詳情可參閱 audio.h。

1.7 核心驅動層

音訊驅動程式可與硬體和 HAL 實現進行互動。我們可以使用高階 Linux 音訊架構 (ALSA)、開放聲音系統 (OSS) 或自定義驅動程式(HAL 與驅動程式無關)。

注意:如果使用的是 ALSA,建議將  external/tinyalsa 用於驅動程式的使用者部分,因為它具有相容的許可(標準的使用者模式庫已獲得 GPL 許可)。

2. 音訊系統架構的演進

一個好的系統架構,需要儘可能地降低上層與具體硬體的耦合,這既是作業系統的設計目的,對於音訊系統也是如此。音訊系統的雛形框架可以簡單的用下圖來表示:

基於Android的音樂播放器的設計與實現

在這個圖中,除去Linux本身的Audio驅動外,整個Android音訊實現都被看成了User。因而我們可以認為Audio Driver就是上層與硬體間的“隔離板”。但是如果單純採用上圖所示的框架來設計音訊系統,對上層應用使用音訊功能是不小的負擔,顯然Android開發團隊還會根據自身的實際情況來進一步細化“User”部分。具體該怎麼細化呢?如果是讓我們去細化我們該怎麼做呢?

首先作為一個作業系統要對外提供可用的API,供應用開發者呼叫。APP開發者開發的應用我們稱APP,我們提供的API姑且叫Framework。如果Framework直接和驅動互動有什麼問題呢?

  1. 首先是耦合問題,介面和實現耦合,硬體層有任何變動都需要介面層適配,我們增加一層硬體適配層;
  2. 資源統一管理的問題,如果多個APP呼叫相同API使用硬體資源,改怎麼分配?增加統一資源管理器,其實就是對應Android系統的Audio Lib層。

細化後我們發現,整個結構對應的就就是Android的幾個層次結構,包括應用層、framework層、庫層以及HAL層,如下圖所示:


基於Android的音樂播放器的設計與實現

我們可以結合目前已有的知識,我們分析Lib層和HAL層架構主要設計思路。

2.1 Lib層

framework層的大多數類,其實只是應用程式使用Android庫檔案的“中介”,它只是個殼子。因為Android應用採用java語言編寫,它們需要最直接的java介面的支援,如果我們的Android系統支援另一種語言的執行時,那麼可以提供另一種語言的介面支援(比如Go),這就是framework層存在的意義之一。但是作為“中介”,它們並不會真正去實現具體的功能,或者只實現其中的一部分功能,而把主要重心放在核心庫中來完成。比如上面的AudioTrack、AudioRecorder、MediaPlayer和MediaRecorder等等在庫中都能找到相對應的類,這些多數是C++語言編寫的。

我們再從另一個線索來思考這個問題:我們提供的API供應用層呼叫,那麼這個API最終執行在應用的程式中。如果多個應用同時使用這個功能就會衝突;再一個允許任何一個程式操作硬體也是個危險的行為。那麼真相就浮出了水面:我們需要一個有許可權管理和硬體互動的程式,需要呼叫某個硬體服務必須和我這個服務打交道。這就是Android系統的很常用的C/S結構以及Binder存在的主要原因。Android系統中的Server就是一個個系統服務,比如ServiceManager、LocationManagerService、ActivityManagerService等等,以及管理影像合成的SurfaceFlinger,和今天我們今天介紹的音訊服務AudioFlinger和AudioPolicyService。它們的程式碼放置在 frameworks/av/services/audioflinger,生成的最主要的庫叫做libaudioflinger。

這裡也提到了分析原始碼除以模組為線索外的另一種線索以程式為線索。庫並不代表一個程式,但是程式則依賴於庫來執行。雖然有的類是在同一個庫中實現的,但並不代表它們會在同一個程式中被呼叫。比如AudioFlinger和AudioPolicyService都駐留於名為mediaserver的系統程式中;而AudioTrack/AudioRecorder和MediaPlayer/MediaRecorder只是應用程式的一部分,它們透過binder服務來與其它audioflinger等系統程式通訊。

2.2 HAL層

硬體抽象層顧名思義為適配不同硬體而獨立封裝的一層,音訊硬體抽象層的任務是將AudioFlinger/AudioPolicyService真正地與硬體裝置關聯起來,但又必須提供靈活的結構來應對變化。

從設計上來看,硬體抽象層是AudioFlinger直接訪問的物件。這裡體現了兩方面的考慮:

  • 一方面AudioFlinger並不直接呼叫底層的驅動程式;
  • 另一方面,AudioFlinger上層(包括和它同一層的MediaPlayerService)的模組只需要與它進行互動就可以實現音訊相關的功能了。

AudioFlinger和HAL是整個架構解耦的核心層,透過HAL層的audio.primary等庫抹平音訊裝置間的差異,無論硬體如何變化,不需要大規模地修改上層實現,保證系統對外暴露的上層API不需要修改,達成高內聚低耦合。而對廠商而言,在定製時的重點就是如何在這部分庫中進行高效實現了。

舉個例子,以前Android系統中的Audio系統依賴於ALSA-lib,但後期就變為了tinyalsa,這樣的轉變不應該對上層造成破壞。因而Audio HAL提供了統一的介面來定義它與AudioFlinger/AudioPolicyService之間的通訊方式,這就是audio_hw_device、audio_stream_in及audio_stream_out等等存在的目的,這些Struct資料型別內部大多隻是函式指標的定義,是一個個控制程式碼。當AudioFlinger/AudioPolicyService初始化時,它們會去尋找系統中最匹配的實現(這些實現駐留在以audio.primary. ,audio.a2dp.為名的各種庫中)來填充這些“殼”,可以理解成是一種“多型”的實現。

3. Linux平臺下的兩種主要的音訊驅動架構介紹

上面我們的示例提到了ALSA,這個其實是Linux平臺的一種音訊驅動架構。下面介紹兩種常見的Linux音訊驅動架構。

3.1 OSS (Open Sound System)

早期Linux版本採用的是OSS框架,它也是Unix及類Unix系統中廣泛使用的一種音訊體系。OSS既可以指OSS介面本身,也可以用來表示介面的實現。OSS的作者是Hannu Savolainen,就職於4Front Technologies公司。由於涉及到智慧財產權問題,OSS後期的支援與改善不是很好,這也是Linux核心最終放棄OSS的一個原因。

另外,OSS在某些方面也遭到了人們的質疑,比如:

  • 對新音訊特性的支援不足;

  • 缺乏對最新核心特性的支援等等。

當然,OSS做為Unix下統一音訊處理操作的早期實現,本身算是比較成功的。它符合“一切都是檔案”的設計理念,而且做為一種體系框架,其更多地只是規定了應用程式與作業系統音訊驅動間的互動,因而各個系統可以根據實際的需求進行定製開發。總的來說,OSS使用瞭如下表所示的裝置節點:

裝置節點 說明
/dev/dsp 向此檔案寫資料à輸出到外放Speaker向此檔案讀資料à從Microphone進行錄音
/dev/mixer 混音器,用於對音訊裝置進行相關設定,比如音量調節
/dev/midi00 第一個MIDI埠,還有midi01,midi02等等
/dev/sequencer 用於訪問合成器(synthesizer),常用於遊戲等效果的產生

更多詳情,可以參考OSS的官方說明

3.2 ALSA(Advanced Linux Sound Architecture)

ALSA是Linux社群為了取代OSS而提出的一種框架,是一個原始碼完全開放的系統(遵循GNU GPL和GNU LGPL)。ALSA在Kernel 2.5版本中被正式引入後,OSS就逐步被排除在核心之外。當然,OSS本身還是在不斷維護的,只是不再為Kernel所採用而已。

ALSA相對於OSS提供了更多,也更為複雜的API介面,因而開發難度相對來講加大了一些。為此,ALSA專門提供了一個供開發者使用的工具庫,以幫助他們更好地使用ALSA的API。根據官方文件的介紹,ALSA有如下特性:

  • 高效支援大多數型別的audio interface(不論是消費型或者是專業型的多聲道音效卡)
  • 高度模組化的聲音驅動
  • SMP及執行緒安全(thread-safe)設計
  • 在使用者空間提供了alsa-lib來簡化應用程式的編寫
  • 與OSS API保持相容,這樣子可以保證老的OSS程式在系統中正確執行

ALSA主要由下表所示的幾個部分組成:

Element Description
alsa-driver 核心驅動包
alsa-lib 使用者空間的函式庫
alsa-utils 包含了很多實用的小程式,比如alsactl:用於儲存裝置設定amixer:是一個命令列程式,用於聲量和其它聲音控制alsamixer:amixer的ncurses版acconnect和aseqview:製作MIDI連線,以及檢查已連線的埠列表aplay和arecord:兩個命令列程式,分別用於播放和錄製多種格式的音訊
alsa-tools 包含一系列工具程式
alsa-firmware 音訊韌體支援包
alsa-plugins 外掛包,比如jack,pulse,maemo
alsa-oss 用於相容OSS的模擬包
pyalsa 用於編譯Python版本的alsa lib

Alsa主要的檔案節點如下:

  1. Information Interface (/proc/asound)
  2. Control Interface (/dev/snd/controlCX)
  3. Mixer Interface (/dev/snd/mixerCXDX)
  4. PCM Interface (/dev/snd/pcmCXDX)
  5. Raw MIDI Interface (/dev/snd/midiCXDX)
  6. Sequencer Interface (/dev/snd/seq)
  7. Timer Interface (/dev/snd/timer)

Android的TinyALSA是基於Linux ALSA基礎改造而來。一看“Tiny”這個詞,我們應該能猜到這是一個ALSA的縮減版本。實際上在Android系統的其它地方也可以看到類似的做法——既想用開源專案,又嫌工程太大太繁瑣,怎麼辦?那就只能瘦身了,於是很多Tiny-XXX就出現了。

在早期版本中,Android系統的音訊架構主要是基於ALSA的,其上層實現可以看做是ALSA的一種“應用”。後來可能是由於ALSA所存在的一些不足,Android後期版本開始不再依賴於ALSA提供的使用者空間層的實現。HAL層最終依賴alsa-lib庫與驅動層互動。

4. 一種新的錄音方式實現

除了之前提到的系統API,我們還有其他的錄音方式嗎?答案是肯定的。上面我們提到HAL層依賴alsa-lib庫與驅動層互動,我們直接使用alsa-lib,繞開HAL層和Framework層不也可以做到嗎(當然前提是要有系統許可權)?

為什麼會有這種述求呢?在做家居和車載產品時,會有四麥、六麥、甚至八麥的場景。錄製大於2麥的裝置時需要在HAL層以及Framework層做適配,基於AOSP的修改會顯得特別重,特別是一些像回聲抑制,聲源定位等訊號處理演算法,如果整合在作業系統,會有更新升級麻煩的問題,我們可以基於alsa-lib在應用層拿到多路資料呼叫訊號處理演算法,這樣演算法模組升級只需要升級APP即可,不需要升級整個系統。

我們先來看看Android系統自帶的tinyX系列工具。

4.1 tinymix混響器

在root使用者下呼叫tinymix可以檢視硬體驅動支援的混響配置

root@android:/ # tinymixNumber of controls: 7ctl type    num name                                     value0   ENUM    1   Playback Path                            OFF1   ENUM    1   Capture MIC Path                         MIC OFF2   ENUM    1   Voice Call Path                          OFF3   ENUM    1   Voip Path                                OFF4   INT 2   Speaker Playback Volume                  0 05   INT 2   Headphone Playback Volume                0 06   ENUM    1   Modem Input Enable                       ON
root@android:/ #複製程式碼

那麼它裡面的內容是什麼意思呢?

  • 首先我們要知道,一個mixer通常有多個controler,像這個,裡面有7個,然後就分別列出每一個controller的資訊;
  • 首先看第一個:它的編號為0,型別是ENUM型,它目前的值是OFF,它是用來控制音訊輸出通道;
  • 同理,第二個也控制音訊輸入通道;
  • 第三個,通話音訊通道;
  • 第四個IP電話音訊通道;
  • 第五個揚聲器音量,和上層音量值無關;
  • 第六個耳機音量,和上層音量值無關;

一般Playback Path對應的列舉值有:

  1. OFF:關閉
  2. RCV
  3. SPK:揚聲器
  4. HP:耳機帶麥
  5. HP_NO_MIC:耳機無麥
  6. BT:藍芽

那麼我如果像改變某一項的時候,要怎麼設定呢?方法是tinymix ctl value;如果tinymix只跟上控制器的編號,就會把控制器的當前狀態顯示出來:

# tinymix 7Audio linein in: On# tinymix 7 0root@dolphin-fvd-p1:/ # **tinymix 7**Audio linein in: Off
複製程式碼

4.2 tinycap採集器

使用下面命令即可實現錄製並儲存到sd卡:

 tinycap 
Usage: tinycap file.wav [-D card] [-d device] [-c channels] [-r rate] [-b bits] [-p period_size] [-n n_periods] 
 tinycap /sdcard/rec.wav -D 0 -d 0 –c 4 –r 16000 –b 16 –p 1024 –n 3
複製程式碼

4.3 tinyplay播放

tinyplay
Usage: tinyplay file.wav [-D card] [-d device] [-p period_size] [-n n_periods]
tinyplay /sdcard/test44.wav -D 0 -d 0 -p 1024 -n 3
複製程式碼

4.4 程式中整合

現在我們已經透過命令的方式實現了繞開framework的音訊採集,我們在自己的app中怎麼使用呢?如果還是透過命令的方式只能錄製到檔案,無法實現流式錄製。

解決辦法是我們的app依賴tinyalsa庫

        struct pcm_config config;
        config.channels = 4;
    config.rate = 16000;
    config.period_size = 1024;
    config.period_count = 4;
    config.start_threshold = 0;
    config.stop_threshold = 0;
    config.silence_threshold = 0;    if (bitDepth == 32)
        config.format = PCM_FORMAT_S32_LE;    else if (bitDepth == 16)
        config.format = PCM_FORMAT_S16_LE;
    pcm = pcm_open(0, device, PCM_IN, &config);    if (!pcm || !pcm_is_ready(pcm)) {        return -1;
    }    int bufferSize = pcm_get_buffer_size(pcm);    char *buffer = (char*)malloc(bufferSize);    int i = pcm_read(pcm, buffer, bufferSize);    if(i ==0){        //success
    }
複製程式碼

5. 總結

本文介紹了Andorid系統的整套音訊架構,以及架構各層級的功能及作用。並介紹了一種繞開framework層的新的音訊採集方式。其實Andorid的音訊架構實現是更復雜的一個過程,本文只是簡略的對各個模組做了一些介紹,以助於更深入理解上一篇提到的各個API的實現。其實API提供出來的音訊介面,都是屬於介面層,不論是Java介面還是C++介面,都隸屬於應用程式。以採集為例,不論我們呼叫哪個API,我們都會發現啟動後應用程式會多出一個AudioRecord的執行緒:

基於Android的音樂播放器的設計與實現

我們啟動的錄製執行緒呼叫API只是從AudioRecord執行緒寫入到Buffer的資料的讀取。

更多Android技術分享可以關注@我,也可以加入QQ群號:1078469822,學習交流Android開發技能。

作者:輕口味
連結:
來源:掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2793817/,如需轉載,請註明出處,否則將追究法律責任。

相關文章