Android系統提供了四個層面的音頻API:
- Java層MediaRecorder&MediaPlayer系列;
- Java層AudioTrack&AudioRecorder系列;
- Jni層opensles;
- JNI層AAudio(Android O引入)
下面分別介紹這些API的使用及特點,
1. MediaRecorder&MediaPlayer
MediaRecorder與MediaPlayer并不能算完整意義的音頻API,它們只是系統音頻API的封裝,除了采集/播放,他們集成了編碼/解碼、復用/解復用等能力,它們在最底層還是呼叫了AudioRecorder、AudioTrack,下面主要介紹它們的幾個主要的配置項,
1.1 MediaRecorder
MediaRecorder因為已經集成了錄音、壓縮編碼、封裝復用等功能,所以使用起來相對比較簡單,
MediaRecorder 使用起來相對簡單,主要設定以下幾項:
- 音源setAudioSource,系統提供的選項:
- AudioSource.DEFAULT:默認音頻源;
- AudioSource.MIC:麥克風;
- AudioSource.VOICE_UPLINK:上行電話錄音,android.Manifest.permission#CAPTURE_AUDIO_OUTPUT;
- AudioSource.VOICE_DOWNLINK:下行電話錄音,android.Manifest.permission#CAPTURE_AUDIO_OUTPUT;
- AudioSource.VOICE_CALL:上下行電話錄音,android.Manifest.permission#CAPTURE_AUDIO_OUTPUT;
- AudioSource.CAMCORDER:設定錄音來源于同方向的相機麥克風相同,若相機無內置相機或無法識別,則使用預設的麥克風
- AudioSource.VOICE_RECOGNITION:用于語音識別;
- AudioSource.VOICE_COMMUNICATION:用于語音通話;
- AudioSource.UNPRECESSED:原始音頻;
- AudioSource.VOICE_PERFORMANCE:低延遲用于滿足實時音頻處理;
- AudioSource.REMOTE_SUBMIX:用于傳輸系統混音的音頻流到遠端, android.Manifest.permission.CAPTURE_AUDIO_OUTPUT;
- AudioSource.ECHO_REFERENCE:回聲抑制參考信號,SystemApi,android.Manifest.permission.CAPTURE_AUDIO_OUTPUT;
- AudioSource.RADIO_TUNER:電臺廣播聲音,SystemApi;
- AudioSource.HOTWORD:搶占式的熱詞檢測,SystemApi,
- 編碼器setAudioEncoder
- Audio.Encoder.DEFAULT:
- Audio.Encoder.AMR_NB:
- Audio.Encoder.AMR_WB:
- Audio.Encoder.AAC:
- Audio.Encoder.HE_AAC:
- Audio.Encoder.AAC_ELD:4.1+
- Audio.Encoder.VORBIS:
- Audio.Encoder.OPUS:Android 10+
- 復用器setOutputFormat
- OutputFormat.DEFAULT
- OutputFormat.THREE_GPP:3GP;
- OutputFormat.MPEG_4:mp4;
- OutputFormat.RAW_AMR:.aac or .amr;
- OutputFormat.AMR_NB:amr nb;
- OutputFormat.AMR_WB:amr wb;
- OutputFormat.AAC_ADIF:aac adif;
- OutputFormat.AAC_ADTS:aac adts;
- OutputFormat.OUTPUT_FORMAT_RTP_AVP:網路流;
- OutputFormat.MPEG_2_TS:ts封裝;
- OutputFormat.WEBM:webm 容器;
- OutputFormat.HEIF: heif容器;
- OutputFormat.OGG:ogg容器,
- 輸出檔案路徑setOutputFile
示例代碼:
MediaRecorder recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
recorder.setOutputFile(PATH_NAME);
recorder.prepare();
recorder.start(); // Recording is now started
...
recorder.stop();
recorder.reset(); // You can reuse the object by going back to setAudioSource() step
recorder.release(); // Now the object cannot be reused
上面代碼只是基本使用方式,具體使用還需結合專案具體需求制定具體邏輯,但是MediaRecorder使用時需實體化,所以在不用時一定要記得即時釋放,以免造成記憶體泄漏,
MediaRecorder狀態圖:

總結:MediaRecorder 實作錄音比較簡單的,代碼量相對較少,較為簡明,但也有不足之處,例如輸出檔案格式選擇較少,錄音程序不能暫停等,
1.2 MediaPlayer
MediaPlayer使用示例:
MediaPlayer mediaPlayer = new MediaPlayer();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_ALARM)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
mediaPlayer.setAudioAttributes(audioAttributes);
} else {
mediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM);
}
mediaPlayer.reset();
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
// 播放完成
}
});
mediaPlayer.setDataSource(path);
mediaPlayer.prepare();
mediaPlayer.start();
//or mediaPlayer.prepareAsync()并在setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)設定的回呼中start
1.2.1 狀態圖:

1.2.2setAudioStreamType提供的選項:
- AudioSystem.STREAM_VOICE_CALL:電話通話;
- AudioSystem.STREAM_SYSTEM:系統聲音;
- AudioSystem.STREAM_RING:電話響鈴聲;
- AudioSystem.STREAM_MUSIC:音樂播放;
- AudioSystem.STREAM_ALARM:鬧鐘;
- AudioSystem.STREAM_NOTIFICATION:通知
AudioAttributes用來替代stream types,它可以設定比stream types更多的屬性,有三個方面:
- usage(why):為什么要播放這個聲音?
- content type(what):播放的內容是什么?是可選項,有些usage,比如CONTENT_TYPE_MOVIE是movie usage,
- flags(how):如何影響播放,
1.2.3 setDataSource說明
setDataSource設定播放源,可以是本地檔案也可以是網路檔案,如果是網路檔案和本地比較大的檔案,在主執行緒中呼叫prepare可能會導致ANR,所以盡量用prepareAsync,然后再onPrepared回呼中呼叫start,
2. AudioRecorder&AudioTrack
2.1 AudioRecorder
AndioRecord 類的主要功能是讓各種 Java 應用能夠管理音瞥澩,以便它們通過此類能夠錄制平臺的聲音輸入硬體所收集的聲音,它的實作就是通過 “pulling 同步”(reading讀取)AudioRecord 物件的聲音資料來完成的,在錄音程序中,應用所需要做的就是通過后面三個類方法中的一個去及時地獲取 AudioRecord 物件的錄音資料, AudioRecord 類提供的三個獲取聲音資料的方法分別是 read(byte[], int, int), read(short[], int, int), read(ByteBuffer, int),無論選擇使用哪一個方法都必須事先設定方便用戶的聲音資料的存盤格式,
開始錄音的時候,一個 AudioRecord 需要初始化一個相關聯的聲音buffer,這個 buffer 主要是用來保存新的聲音資料,這個 buffer 的大小,我們可以在物件構造期間去指定,它表明一個 AudioRecord 物件還沒有被讀取(同步)聲音資料前能錄多長的音(即一次可以錄制的聲音容量),聲音資料從音頻硬體中被讀出,資料大小不超過整個錄音資料的大小(可以分多次讀出),即每次讀取初始化 buffer 容量的資料,
采集作業很簡單,我們只需要構造一個AudioRecord物件,然后傳入各種不同配置的引數即可,一般情況下錄音實作的簡單流程如下:
- 音頻源:我們可以使用麥克風作為采集音頻的資料源,可選引數跟MediaRecorder一致,
- 采樣率:一秒鐘對聲音資料的采樣次數,采樣率越高,音質越好,
- 音頻通道:單聲道,雙聲道等,
- 音頻格式:一般選用PCM格式,即原始的音頻樣本,
- 緩沖區大小:音頻資料寫入緩沖區的總數,可以通過AudioRecord.getMinBufferSize獲取最小的緩沖區,(將音頻采集到緩沖區中然后再從緩沖區中讀取),
代碼實作如下:
public class AudioRecorder {
//音頻輸入-麥克風
private final static int AUDIO_INPUT = MediaRecorder.AudioSource.MIC;
//采用頻率
//44100是目前的標準,但是某些設備仍然支持22050,16000,11025
//采樣頻率一般共分為22.05KHz、44.1KHz、48KHz三個等級
private final static int AUDIO_SAMPLE_RATE = 16000;
//聲道 單聲道
private final static int AUDIO_CHANNEL = AudioFormat.CHANNEL_IN_MONO;
//編碼
private final static int AUDIO_ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public void createDefaultAudio() {
// 獲得緩沖區位元組大小
int bufferSizeInBytes = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE,
AUDIO_CHANNEL, AUDIO_ENCODING);
AudioRecord audioRecord = new AudioRecord(AUDIO_INPUT, AUDIO_SAMPLE_RATE, AUDIO_CHANNEL, AUDIO_ENCODING, bufferSizeInBytes);
audioRecord.startRecording();
new Thread(new Runnable() {
@Override
public void run() {
readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
if (AudioRecord.ERROR_INVALID_OPERATION != readsize) {
//handle audio data
}
}
}).start();
}
}
2.2 AudioTrack
AudioTrack是Java層管理和播放單個音瞥澩提供的介面,它用來播放PCM原始資料,通過使用write(byte[], int, int)、write(short[], int, int)和write(float[], int, int, int)方法之一寫入資料實作播放,AudioTrack實體可以在兩種模式下運行:靜態模式或流模式,
-
在流模式下,應用程式使用write()方法之一將連續的資料流寫入AudioTrack,當資料從Java層傳輸到native層并排隊等待播放時,它們會阻塞并回傳,當播放音頻資料塊時,流式模式是最有用的,例如:因為聲音播放的時間太長,所以記憶體無法容納;由于音頻資料的特性(高采樣率,每次采樣的位元數…);在播放之前排隊的音頻時接識訓生成,
-
靜態模式用于處理適合記憶體且需要以最小延遲播放的短聲音場景,因此,靜態模式將更適合UI和游戲聲音,并盡可能減少開銷,
創建AudioTrack物件時,初始化其關聯的音頻緩沖區,在構造程序中指定的這個緩沖區的大小決定了AudioTrack在耗盡資料之前可以播放多長時間,對于使用靜態模式的AudioTrack,此大小是它可以播放的聲音的最大大小,對于流模式,資料將以小于或等于總緩沖區大小的塊寫入音頻接收器,AudioTrack不是final的,因此允許使用子類,但不建議這樣使用,
靜態模式播放示例:
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFormat auidoFormat = new AudioFormat.Builder().setSampleRate(22050)
.setEncoding(AudioFormat.ENCODING_PCM_8BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build();
AudioTrack audioTrack = new AudioTrack(audioAttributes,auidoFormat,audioData.length,AudioTrack.MODE_STATIC,
AudioManager.AUDIO_SESSION_ID_GENERATE);
audioTrack.write(audioData, 0, audioData.length);
if(audioTrack.getState() == AudioTrack.STATE_UNINITIALIZED){
Toast.makeText(this,"AudioTrack初始化失敗!",Toast.LENGTH_SHORT).show();
return;
}
audioTrack.play();
流模式播放示例:
// ************ 流播放 ************
final int minBufferSize = AudioTrack.getMinBufferSize(SAMPLE_RATE_INHZ, AudioFormat.CHANNEL_OUT_MONO, AUDIO_FORMAT);
AudioAttributes audioAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFormat auidoFormat = new AudioFormat.Builder().setSampleRate(22050)
.setEncoding(AudioFormat.ENCODING_PCM_8BIT)
.setChannelMask(AudioFormat.CHANNEL_OUT_MONO)
.build();
audioTrack = new AudioTrack(audioAttributes,auidoFormat,minBufferSize,AudioTrack.MODE_STREAM,AudioManager.AUDIO_SESSION_ID_GENERATE);
// 檢查初始化是否成功
if(audioTrack.getState() == AudioTrack.STATE_UNINITIALIZED){
Toast.makeText(this,"AudioTrack初始化失敗!",Toast.LENGTH_SHORT).show();
return;
}
// 播放
audioTrack.play();
//子執行緒中檔案流寫入
workHandler.post(new Runnable() {
@Override
public void run() {
try {
final File file = new File(getExternalFilesDir(Environment.DIRECTORY_MUSIC), "test.pcm");
FileInputStream fileInputStream = new FileInputStream(file);
byte[] tempBuffer = new byte[minBufferSize];
while (fileInputStream.available() > 0) {
int readCount = fileInputStream.read(tempBuffer);
if (readCount == AudioTrack.ERROR_INVALID_OPERATION ||
readCount == AudioTrack.ERROR_BAD_VALUE) {
continue;
}
if (readCount != 0 && readCount != -1) {
audioTrack.write(tempBuffer, 0, readCount);
}
}
fileInputStream.close();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
});
3. OpenSL ES
OpenSL ES (Open Sound Library for Embedded Systems)是無授權費、跨平臺、針對嵌入式系統精心優化的硬體音頻加速API,它為嵌入式移動多媒體設備上的本地應用程式開發者提供標準化, 高性能,低回應時間的音頻功能實作方法,并實作軟/硬體音頻性能的直接跨平臺部署,降低執行難度,促進高級音頻市場的發展,簡單來說OpenSL ES是一個嵌入式跨平臺免費的音頻處理庫, iOS未暴露OpenSL ES介面,Android的OpenSL ES庫是在NDK的platforms檔案夾對應android平臺先相應cpu型別里面:
$ ls ~/Library/Android/ndk/ndk-bundle/android-ndk-r17c/platforms/android-28/arch-arm/usr/lib/
crtbegin_dynamic.o libGLESv3.so libcompiler_rt-extras.a libnativewindow.so
crtbegin_so.o libOpenMAXAL.so libdl.a libneuralnetworks.so
crtbegin_static.o libOpenSLES.so libdl.so libstdc++.a
crtend_android.o libaaudio.so libjnigraphics.so libstdc++.so
crtend_so.o libandroid.so liblog.so libsync.so
libEGL.so libc.a libm.a libvulkan.so
libGLESv1_CM.so libc.so libm.so libz.a
libGLESv2.so libcamera2ndk.so libmediandk.so libz.so
Android 2.3 (API 9) 即開始支持 OpenSL ES 標準了,通過 NDK 提供相應的 API 開發介面(參考:source.android.com/devices/aud… 實作的 OpenSL ES 只是 OpenSL 1.0.1 的子集,并且進行了擴展,因此,對于 OpenSL ES API 的使用,我們還需要特別留意哪些是 Android 支持的,哪些是不支持的,具體相關檔案的地址位于 NDK docs 目錄下:
NDKroot/docs/Additional_library_docs/opensles/index.html
NDKroot/docs/Additional_library_docs/opensles/OpenSL_ES_Specification_1.0.1.pdf
OpenSL ES 有兩個必須理解的概念,就是 Object 和 Interface,Object 可以想象成 Java 的 Object 類,Interface 可以想象成 Java 的 Interface,但它們并不完全相同,其實就是為了用面向程序的語言實作面向物件的介面,下面進一步解釋他們的關系: (1) 每個 Object 可能會存在一個或者多個 Interface,官方為每一種 Object 都定義了一系列的 Interface (2)每個 Object 物件都提供了一些最基礎的操作,比如:Realize,Resume,GetState,Destroy 等等,如果希望使用該物件支持的功能函式,則必須通過其 GetInterface 函式拿到 Interface 介面,然后通過 Interface 來訪問功能函式 (3)并不是每個系統上都實作了 OpenSL ES 為 Object 定義的所有 Interface,所以在獲取 Interface 的時候需要做一些選擇和判斷
3.1 常用的物件和結構體
在 OpenSL ES 中,一切 API 的訪問和控制都是通過 Interface 來完成的,連 OpenSL ES 里面的 Object 也是通過 SLObjectItf Interface 來訪問和使用的,
3.1.1 Engine Object 和 SLEngineItf Interface
OpenSL ES 里面最核心的物件就是:Engine Object,音頻引擎物件,它主要提供如下兩個功能:
(1)管理 Audio Engine 的生命周期
(2)提供管理介面: SLEngineItf,該介面可以用來創建所有其他的 Object 物件
(3)提供設備屬性查詢介面:SLEngineCapabilitiesItf 和 SLAudioIODeviceCapabilitiesItf,這些介面可以查詢設備的一些屬性資訊
Engine Object 物件的創建方法如下:
SLObjectItf engineObject;
slCreateEngine( &engineObject, 0, nullptr, 0, nullptr, nullptr );1.2.
初始化/銷毀:
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);(*engineObject)->Destroy(engineObject);1.2.
獲取管理介面:
SLEngineItf engineEngine;(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &(engineEngine));1.2.
SLObjectItf結構定義:
struct SLObjectItf_ { SLresult (*Realize) (SLObjectItf self,SLboolean async); SLresult (*Resume) (SLObjectItf self,SLboolean async); SLresult (*GetState) (SLObjectItf self,SLuint32 * pState); SLresult (*GetInterface) (SLObjectItf self, const SLInterfaceID iid, void * pInterface); SLresult (*RegisterCallback) (SLObjectItf self, slObjectCallback callback, void * pContext); void (*AbortAsyncOperation) (SLObjectItf self); void (*Destroy) (SLObjectItf self); SLresult (*SetPriority) (SLObjectItf self, SLint32 priority, SLboolean preemptable); SLresult (*GetPriority) (SLObjectItf self, SLint32 *pPriority, SLboolean *pPreemptable); SLresult (*SetLossOfControlInterfaces) (SLObjectItf self, SLint16 numInterfaces, SLInterfaceID * pInterfaceIDs, SLboolean enabled);};typedef const struct SLObjectItf_ * const * SLObjectItf;
這里的SLObjectItf是一個二級指標,它指向的是一個結構體指標,任何創建出來的Object都必須呼叫 Realize 方法做初始化,在不需要的時候可以使用 Destroy 方法來釋放資源,
下面我們就可以使用 engineEngine 來創建所有 OpenSL ES 的其他物件了,
3.1.2 Media Object
OpenSL ES 里面另一組比較重要的物件就是 Media Object ,代表著多媒體功能的抽象,比如:player、recorder 等等,
我們可以通過 SLEngineItf 提供的 CreateAudioPlayer 方法來創建一個 player 物件實體,可以通過 SLEngineItf 提供的 CreateAudioRecorder 方法來創建一個 recorder 實體,
3.1.3 Data Source 和 Data Sink
OpenSL ES 里面,這兩個結構體均是作為創建 Media Object 物件時的引數而存在的,data source 代表著輸入源的資訊,即資料從哪兒來、輸入的資料引數是怎樣的;而 data sink 則代表著輸出的資訊,即資料輸出到哪兒、以什么樣的引數來輸出,
Data Source 的定義如下:
typedef struct SLDataSource_ { void *pLocator; void *pFormat;} SLDataSource;1.2.3.4.
Data Sink 的定義如下:
typedef struct SLDataSink_ { void *pLocator; void *pFormat;} SLDataSink;1.2.3.4.
其中,pLocator 主要有如下幾種:
SLDataLocator_AddressSLDataLocator_BufferQueueSLDataLocator_IODeviceSLDataLocator_MIDIBufferQueueSLDataLocator_URI1.2.3.4.5.
也就是說,Media Object 物件的輸入源/輸出源,既可以是 URL,也可以 Device,或者來自于緩沖區佇列等等,完全是由 Media Object 物件的具體型別和應用場景來配置,
3.2 狀態機制
OpenSL ES 還有一個比較重要的概念,就是它的狀態機制,如圖所示:

任何一個 OpenSL ES 的物件,創建成功后,都進入 SL_OBJECT_STATE_UNREALIZED 狀態,這種狀態下,系統不會為它分配任何資源,直到呼叫 Realize 函式為止,
Realize 后的物件,就會進入 SL_OBJECT_STATE_REALIZED 狀態,這是一種“可用”的狀態,只有在這種狀態下,物件的各個功能和資源才能正常地訪問,
當一些系統事件發生后,比如出現錯誤或者 Audio 設備被其他應用搶占,OpenSL ES 物件會進入 SL_OBJECT_STATE_SUSPENDED 狀態,如果希望恢復正常使用,需要呼叫 Resume 函式,
當呼叫物件的 Destroy 函式后,則會釋放資源,并回到 SL_OBJECT_STATE_UNREALIZED 狀態,
簡言之,一個 OpenSL ES 物件的生命周期,就是從 create 到 destroy 的程序,生命周期的控制,都是通過開發者顯示呼叫來完成的,
3.3 API呼叫流程總結
Player: 
Recorder: 
更多API細節可以參考官方提供的《OpenSL_ES_Specification_1.0.1.pdf》,播放邏輯可參考oarplayer中的 github.com/qingkouwei/… 音頻輸出代碼,
4. AAudio & Oboe
AAudio 是在 Android O 版本中引入的全新 Android C API,此 API 專為需要低延遲的高性能音頻應用而設計,應用通過讀取資料并將資料寫入流來與 AAudio 進行通信,
AAudio API 采用最精簡的設計,不執行以下功能:
- 音頻設備列舉
- 音頻端點之間的自動化路由
- 檔案 IO
- 解碼壓縮的音頻
- 在單一回呼中自動呈交所有輸入/流,
每個流都連接到單個音頻設備,音頻設備是硬體介面或虛擬端點,用作連續的數字音頻資料流的來源或接收器,不要將音頻設備(內置麥克風或藍牙耳機)與運行應用的 Android 設備(手機或智能手表)混淆,可以使用 AudioManager 方法 getDevices() 來發現 Android 設備上可用的音頻設備,該方法會回傳每個設備 type 的相關資訊,Android 設備上的每個音頻設備都具有唯一 ID,可以使用該 ID 將音頻流與特定音頻設備系結,但是,在大多數情況下,可以讓 AAudio 選擇默認的主要設備,無需自己指定,接到流的音頻設備負責確定該流是用于輸入還是輸出,流只能在一個方向上移動資料,定義流時,您還可以設定其方向,打開流時,Android 會執行檢查,確保音頻設備與流方向一致,
4.1 API說明
4.1.1 AAudio創建音頻流流程
AAudio 庫使用了構建器設計模式,并提供 AAudioStreamBuilder,
-
創建 AAudioStreamBuilder:
AAudioStreamBuilder *builder;aaudio_result_t result = AAudio_createStreamBuilder(&builder); -
使用與流引數對應的構建器函式,在構建器中設定音頻流配置,可供使用的可選設定函式如下所示:
AAudioStreamBuilder_setDeviceId(builder, deviceId);AAudioStreamBuilder_setDirection(builder, direction);AAudioStreamBuilder_setSharingMode(builder, mode);AAudioStreamBuilder_setSampleRate(builder, sampleRate);AAudioStreamBuilder_setChannelCount(builder, channelCount);AAudioStreamBuilder_setFormat(builder, format);AAudioStreamBuilder_setBufferCapacityInFrames(builder, frames);注意:這些方法不會報告諸如常量未定義或值超出范圍之類的錯誤,
如果未指定 deviceId,將默認為主要輸出設備,如果您未指定流方向,將默認為輸出流,對于所有其他引數,可明確設定值,也可以完全不指定引數或將其設定為
AAUDIO_UNSPECIFIED,讓系統分配最佳值,為安全起見,在創建音頻流之后,需要檢查其狀態, -
配置 AAudioStreamBuilder 之后,用其創建流:
AAudioStream *stream;result = AAudioStreamBuilder_openStream(builder, &stream); -
創建流之后,驗證其配置,如果已指定采樣格式、采樣率或每幀樣本數,這些設定不會變更,如果已指定共享模式或緩沖區容量,這些設定可能會變更,具體取決于流的音頻設備能力,以及運行該流的 Android 設備,作為一種良好的防御性編程習慣,您應該先檢查流的配置,然后再使用,您可使用相應函式檢索與每項構建器設定對應的流設定:
AAudioStreamBuilder_setDeviceId()AAudioStream_getDeviceId()AAudioStreamBuilder_setDirection()AAudioStream_getDirection()AAudioStreamBuilder_setSharingMode()AAudioStream_getSharingMode()AAudioStreamBuilder_setSampleRate()AAudioStream_getSampleRate()AAudioStreamBuilder_setChannelCount()AAudioStream_getChannelCount()AAudioStreamBuilder_setFormat()AAudioStream_getFormat()AAudioStreamBuilder_setBufferCapacityInFrames()AAudioStream_getBufferCapacityInFrames() -
可以保存該構建器,以便將來再用其創建更多流,但是,如果不打算再使用該構建器,則應將其洗掉,
AAudioStreamBuilder_delete(builder);
4.1.2 AAudio使用音頻流
僅當流處于“已開始”狀態時,資料才會通過流來流動,,使用以下函式請求狀態轉換:
aaudio_result_t result;result = AAudioStream_requestStart(stream);result = AAudioStream_requestStop(stream);result = AAudioStream_requestPause(stream);result = AAudioStream_requestFlush(stream);
4.1.3 AAudio讀取和寫入音頻流
在流啟動后,可通過兩種方法來處理流中的資料:
- 使用高優先級回呼,
- 使用函式
AAudioStream_read(stream, buffer, numFrames, timeoutNanos)和AAudioStream_write(stream, buffer, numFrames, timeoutNanos)來讀取或寫入流,
對于傳輸指定幀數的阻塞讀取或寫入操作,請將 timeoutNanos 設定為大于零, 對于非阻塞呼叫,請將 timeoutNanos 設定為零,在這種情況下,結果將是傳輸的實際幀數,
讀取輸入值時,您應驗證是否已讀取正確數量的幀,如果未讀取正確數量的幀,緩沖區可能包含未知的資料,從而引起音頻干擾,您可以在緩沖區中填入零,以產生靜音效果:
aaudio_result_t result = AAudioStream_read(stream, audioData, numFrames, timeout);if (result < 0) { // Error!}if (result != numFrames) { // pad the buffer with zeros memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0, sizeof(sample_type) * (numFrames - result) * samplesPerFrame);}
4.1.4 AAudio關閉音頻流
使用完流之后,請將其關閉:
AAudioStream_close(stream);
關閉流之后,便無法將其與任何基于流的 AAudio 函式配合使用,
4.1.5 AAudio斷開連接的音頻流
如果發生以下任一事件,音頻流隨時可能會斷開連接:
- 關聯的音頻設備不再處于連接狀態(例如,在拔出頭戴式耳機時),
- 發生內部錯誤,
- 某音頻設備不再是主要音頻設備,
流斷開連接時,其狀態為“已斷開連接”,所有嘗試執行 AAudioStream_write 或其他函式的操作都會回傳錯誤,無論是什么錯誤代碼,您始終必須停止并關閉已斷開連接的流,
如果您使用的是資料回呼(而不是某個直接讀取/寫入方法),那么當流斷開連接時,您不會收到任何回傳代碼,如需在發生這種情況時接收通知,請撰寫 AAudioStream_errorCallback 函式,然后使用 AAudioStreamBuilder_setErrorCallback() 注冊該函式,
果您在錯誤回呼執行緒中收到連接已斷開的通知,則流的停止和關閉必須從其他執行緒中完成,否則可能出現死鎖,
請注意,如果打開新的流,其設定可能與原始流不同(例如,framesPerBurst):
void errorCallback(AAudioStream *stream, void *userData, aaudio_result_t error) { // Launch a new thread to handle the disconnect. std::thread myThread(my_error_thread_proc, stream, userData); myThread.detach(); // Don't wait for the thread to finish.}
4.2 狀態轉換
AAudio 流一般有以下五種穩定狀態(本部分結尾將介紹錯誤狀態 Disconnected):
- 打開
- 已開始
- 已暫停
- 已重繪
- 已停止
狀態切換函式是異步函式,因此狀態不會立即變更,當您請求變更狀態時,流會進入以下相應的過渡狀態:
- 正在開始
- 正在暫停
- 正在重繪
- 正在停止
- 正在關閉
以下狀態圖將穩定狀態顯示為圓角矩形,而將過渡狀態顯示為虛線矩形,盡管未顯示,但您可從任意狀態呼叫 close()

AAudio 未提供回呼函式來提醒您狀態發生變更,您可使用特殊函式 AAudioStream_waitForStateChange(stream, inputState, nextState, timeout) 等待狀態變更,
此函式本身并不會檢測狀態變更情況,也不會等待特定的狀態,而是等待當前狀態偏離您指定的 inputState,
例如,請求暫停后,流應立即進入“正在暫停”過渡狀態,并在稍后某個時刻進入“已暫停”狀態,但不保證一定如此,由于無法等待“已暫停”狀態,請使用 waitForStateChange() 來等待除“正在暫停”之外的任何狀態,方法如下:
aaudio_stream_state_t inputState = AAUDIO_STREAM_STATE_PAUSING;aaudio_stream_state_t nextState = AAUDIO_STREAM_STATE_UNINITIALIZED;int64_t timeoutNanos = 100 * AAUDIO_NANOS_PER_MILLISECOND;result = AAudioStream_requestPause(stream);result = AAudioStream_waitForStateChange(stream, inputState, &nextState, timeoutNanos);
如果流的狀態并非“正在暫停”(即 inputState,我們假定這就是當前執行呼叫時的狀態),該函式會立即回傳,否則,函式會阻止運行,直至狀態不再是“正在暫停”,或者超時,當函式回傳時,引數 nextState 會顯示流的當前狀態,
您可在呼叫開始、停止或重繪請求后使用這種方法,將相應的過渡狀態用作 inputState,不要在呼叫 AAudioStream_close() 之后呼叫 waitForStateChange(),因為流在關閉時會立即被洗掉,此外,不要在另一執行緒運行 waitForStateChange() 時呼叫 AAudioStream_close(),
4.3 優化性能
我們可以通過調整內部緩沖區,使用特殊的高優先級執行緒,優化音頻應用的性能,
4.3.1 調整緩沖區以最大限度減少延遲時間
AAudio 會將資料傳入其維護的內部緩沖區,并從中傳出資料(每個音頻設備各有一個內部緩沖區),
注意:不要將 AAudio 的內部緩沖區與 AAudio 流讀取或寫入函式的緩沖區引數混淆,
緩沖區的容量是緩沖區中可以存放的資料總量,我們可以呼叫 AAudioStreamBuilder_setBufferCapacityInFrames() 來設定容量,這個方法將可分配的容量限制為設備允許的最大值,使用 AAudioStream_getBufferCapacityInFrames() 可以驗證緩沖區的實際容量,
應用不必使用緩沖區的全部容量,可以設定 AAudio 填充緩沖區空間的大小上限,緩沖區空間大小不得超過它的容量,而且通常小于其容量,可以通過控制緩沖區大小,確定填充緩沖區所需的脈沖串數,從而控制延遲時間,可以使用 AAudioStreamBuilder_setBufferSizeInFrames() 和 AAudioStreamBuilder_getBufferSizeInFrames() 方法來處理緩沖區大小,
應用播放音頻時,會將資料寫入緩沖區并阻止運行,直至寫入完成,AAudio 以離散的脈沖串從緩沖區中讀取資料,每個脈沖串都包含多個音頻幀,而且通常小于所讀取的緩沖區大小,脈沖串的大小及速率由系統控制,而這些屬性通常由音頻設備的電路指定,雖然無法更改脈沖串的大小或速率,但可以根據內部緩沖區所含的脈沖串數量來設定內部緩沖區大小,通常,當 AAudioStream 的緩沖區大小是所報告脈沖串大小的倍數時,延遲時間最短,

優化緩沖區空間大小的一種方法是從較大的緩沖區開始,逐漸將其減小直至開始出現緩沖區不足現象,再稍稍將其調大,此外,也可以從較小的緩沖區空間大小開始,如果出現緩沖區不足現象,則增大緩沖區空間大小,直至輸出再次流暢為止,這個程序推進速度很快,很可能在用戶開始播放第一個音頻前就已完成,可以先以靜音執行初始緩沖區大小調整,確保用戶不會聽到任何音頻干擾聲,隨著時間推移,系統性能可能會有所變化(例如,用戶可能會關閉飛行模式),因為緩沖區調整所產生的開銷非常小,應用可以在對流讀取或寫入資料的同時,連續不斷地調整緩沖區,
以下是緩沖區優化回圈的示例:
int32_t previousUnderrunCount = 0;int32_t framesPerBurst = AAudioStream_getFramesPerBurst(stream);int32_t bufferSize = AAudioStream_getBufferSizeInFrames(stream);int32_t bufferCapacity = AAudioStream_getBufferCapacityInFrames(stream);while (go) { result = writeSomeData(); if (result < 0) break; // Are we getting underruns? if (bufferSize < bufferCapacity) { int32_t underrunCount = AAudioStream_getXRunCount(stream); if (underrunCount > previousUnderrunCount) { previousUnderrunCount = underrunCount; // Try increasing the buffer size by one burst bufferSize += framesPerBurst; bufferSize = AAudioStream_setBufferSize(stream, bufferSize); } }}
對于輸入流來說,使用這種方法優化緩沖區大小并無益處,輸入流以盡可能快的速度運行,以嘗試將快取資料量保持在最低限度,然后在應用被搶占時填補緩沖區,
4.3.2 使用高優先級回呼
如果應用從原始執行緒中讀取或寫入音頻資料,可能會被搶占或遇到定時抖動,進而可能引起音頻干擾,使用較大的緩沖區有助于避免此類干擾,但是如果緩沖區較大,音頻延遲時間也會更長,對于要求延遲時間較短的應用,音頻流可以使用一個異步回呼函式,將資料傳輸到應用并從中傳輸資料,AAudio 會在優先級較高的執行緒中執行該回呼,這有助于改善性能,
該回呼函式的原型如下所示:
typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)( AAudioStream *stream, void *userData, void *audioData, int32_t numFrames);
使用流構建方式來注冊回呼:
AAudioStreamBuilder_setDataCallback(builder, myCallback, myUserData);
在最簡單的情況下,流會定期執行該回呼函式,以獲取用于下一個脈沖串的資料,
該回呼函式不應對呼叫它的流執行讀取或寫入操作,如果該回呼屬于某個輸入流,那么您的代碼應處理在 audioData 緩沖區(指定為第三個引數)中提供的資料,如果該回呼屬于某個輸出流,那么您的代碼應將資料放入該緩沖區,
例如,可以使用回呼來連續生成正弦波輸出,如下所示:
aaudio_data_callback_result_t myCallback( AAudioStream *stream, void *userData, void *audioData, int32_t numFrames) { int64_t timeout = 0; // Write samples directly into the audioData array. generateSineWave(static_cast<float *>(audioData), numFrames); return AAUDIO_CALLABCK_RESULT_CONTINUE;}
使用 AAudio 可以處理多個流,可以將其中一個流用作主流,并在用戶資料中傳遞指向其他流的指標,針對主流注冊回呼,然后,對其他流使用非阻塞 I/O,以下是將輸入流傳遞到輸出流的往回傳呼示例,主呼叫流是輸出流,輸入流包括在用戶資料中,
該回呼從輸入流執行非阻塞讀取,以將資料放入輸出流的緩沖區:
aaudio_data_callback_result_t myCallback( AAudioStream *stream, void *userData, void *audioData, int32_t numFrames) { AAudioStream *inputStream = (AAudioStream *) userData; int64_t timeout = 0; aaudio_result_t result = AAudioStream_read(inputStream, audioData, numFrames, timeout); if (result == numFrames) return AAUDIO_CALLABCK_RESULT_CONTINUE; if (result >= 0) { memset(static_cast<sample_type*>(audioData) + result * samplesPerFrame, 0, sizeof(sample_type) * (numFrames - result) * samplesPerFrame); return AAUDIO_CALLBACK_RESULT_CONTINUE; } return AAUDIO_CALLBACK_RESULT_STOP;}
請注意,在此示例中,假定輸入流和輸出流的通道數量、格式和采樣率均相同,流的格式可以不匹配,只要代碼正確處理轉換即可,
4.3.3 設定性能模式
每個 AAudioStream 都具有性能模式,而這對應用行為的影響很大,共有三種模式:
AAUDIO_PERFORMANCE_MODE_NONE是默認模式,這種模式使用在延遲時間與節能之間取得平衡的基本流,AAUDIO_PERFORMANCE_MODE_LOW_LATENCY使用較小的緩沖區和經優化的資料路徑,以減少延遲時間,AAUDIO_PERFORMANCE_MODE_POWER_SAVING使用較大的內部緩沖區,以及以延遲時間為代價換取節能優勢的資料路徑,
可以通過呼叫 setPerformanceMode() 來選擇性能模式,并通過呼叫 getPerformanceMode() 來發現當前模式,
如果在應用中縮短延遲時間比節能更重要,請使用 AAUDIO_PERFORMANCE_MODE_LOW_LATENCY,這對互動性非常強的應用(例如游戲或鍵盤合成器)非常有用,
如果在您的應用中節能比縮短延遲時間更重要,請使用 AAUDIO_PERFORMANCE_MODE_POWER_SAVING,對于回放先前生成的音樂的應用(例如流式音頻或 MIDI 檔案播放器),情況通常如此,
在當前版本的 AAudio 中,為了盡量減少延遲時間,必須將 AAUDIO_PERFORMANCE_MODE_LOW_LATENCY 性能模式與高優先級回呼配合使用,請參閱以下示例:
// Create a stream builderAAudioStreamBuilder *streamBuilder;AAudio_createStreamBuilder(&streamBuilder);AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);// Use it to create the streamAAudioStream *stream;AAudioStreamBuilder_openStream(streamBuilder, &stream);
官方檔案:developer.android.google.cn/ndk/guides/…
4.4 Oboe
谷歌開源了Oboe庫,Oboe 是一個 C++ 封裝容器,提供與 AAudio 非常相似的 API,它在 AAudio 可用時對其進行呼叫,并在 AAudio 不可用時回退使用 OpenSL ES,
以錄音為例實作程式:
class Callback:public oboe::AudioStreamCallback{DataCallbackResultOboeRecorder::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) { uint32_t size = 2 * numFrames * channels; //handle audio data return DataCallbackResult::Continue;}}oboe::AudioStreamBuilder builder;builder->setAudioApi(mAudioApi) ->setFormat(mFormat) ->setSharingMode(oboe::SharingMode::Exclusive) ->setPerformanceMode(oboe::PerformanceMode::LowLatency) ->setDeviceId(mRecordingDeviceId) ->setFramesPerCallback(framesPerBuffer) ->setDirection(oboe::Direction::Input) ->setDataCallback(new Callback()) ->setSampleRate(samplerate) ->setChannelCount(channels);oboe::AudioStream *mRecordingStream = nullptr;oboe::Result result = builder.openStream(&mRecordingStream);if (result == oboe::Result::OK && mRecordingStream) { mRealSampleRate = mRecordingStream->getSampleRate(); mFormat = mRecordingStream->getFormat(); oboe::Result result = stream->requestStart(); mRecordingStream->waitForStateChange(StreamState::Starting, &state, 10*kNanosPerMillisecond); if (result != oboe::Result::OK) { Logg("Error starting stream. %s", oboe::convertToText(result)); }} else { Logg("Failed to create recording stream. Error: %s",oboe::convertToText(result)); result = mRecordingStream->close();}
Oboe提供了很多創意的參考,可以直接在Oboe github倉庫查看,
5. 總結
本篇介紹了Android提供的音頻采集/播放介面,Java層可以使用AudioRecorder/AudioTrack,jni層推薦使用Oboe,不論哪個API都是最終通過framework層、HAL層與硬體打交道,音頻采集時在應用層會有一個“AudioRecord”的執行緒來與framework的服務互動,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/300830.html
標籤:其他
