AAudio 是針對 Android O 版本推出的全新 Android C API,此款 API 專為需要短延遲時間的高效能音訊應用程式設計。應用程式會透過讀取及寫入資料串流,與 AAudio 通訊。
AAudio API 採用最精簡的設計,不會執行以下功能:
- 音訊裝置列舉
- 在音訊端點之間自動設定路由
- 檔案 I/O
- 將壓縮音訊解碼
- 在單一回呼中自動顯示所有輸入/串流。
開始使用
您可以透過 C++ 程式碼呼叫 AAudio。如要為應用程式新增 AAudio 功能集,請加入 AAudio.h 標頭檔案:
#include <aaudio/AAudio.h>
音訊串流
AAudio 會在應用程式與 Android 裝置的音訊輸入和輸出之間移動音訊資料。您的應用程式會透過讀取及寫入以 AAudioStream 結構表示的音訊串流,來傳入和傳出資料。讀取/寫入呼叫可能是阻斷式或非阻斷式呼叫。
串流的定義如下:
- 音訊裝置是串流中資料的來源或接收器。
- 共用模式可決定串流是否只具有音訊裝置的專屬存取權,否則音訊裝置可能可供多個串流存取。
- 串流中的音訊資料格式。
音訊裝置
每個串流都連接至單一音訊裝置。
音訊裝置是硬體介面或虛擬端點,可做為連續數位音訊資料流的來源或接收器。請勿將音訊裝置 (內建麥克風或藍牙耳機) 與執行應用程式的 Android 裝置 (手機或智慧手錶) 弄混。
您可以使用 AudioManager
方法 getDevices()
找出可在 Android 裝置上使用的音訊裝置。這個方法會傳回每部裝置的 type
相關資訊。
每部音訊裝置在 Android 裝置上都有專屬 ID。使用這個 ID 即可將音訊串流繫結至特定音訊裝置。不過在大部分情況下,您可以讓 AAudio 選擇預設的主要裝置,不必自行指定裝置。
連接至串流的音訊裝置可決定串流要用於輸入或輸出。串流只能讓資料朝單一方向移動。定義串流時,您也可以設定串流的方向。開啟串流時,Android 會執行檢查,確保音訊裝置和串流方向一致。
共用模式
串流具有共用模式:
AAUDIO_SHARING_MODE_EXCLUSIVE
表示串流對音訊裝置擁有專屬存取權;該裝置無法用於任何其他音訊串流。如果音訊裝置正在使用中,串流便可能無法取得專屬存取權。專屬串流的延遲時間通常較短,但連線中斷的機率也較高。不再需要專屬串流時,應立即關閉,以免其他應用程式存取裝置。專屬串流可以最大程度縮短延遲時間。AAUDIO_SHARING_MODE_SHARED
可讓 AAudio 混合音訊。AAudio 會混合指派給同一部裝置的所有共用串流。
您可以在建立串流時明確設定共用模式。根據預設,共用模式為 SHARED
。
音訊格式
透過串流傳送的資料會採用常用的數位音訊屬性,如下所示:
- 取樣資料格式
- 聲道數 (每影格取樣數)
- 取樣率
AAudio 允許使用以下取樣格式:
aaudio_format_t | C 資料類型 | 附註 |
---|---|---|
AAUDIO_FORMAT_PCM_I16 | int16_t | 常見的 16 位元樣本 (Q0.15 格式) |
AAUDIO_FORMAT_PCM_FLOAT | 浮點值 | -1.0 至 +1.0 |
AAUDIO_FORMAT_PCM_I24_PACKED | uint8_t (3 個一組) | 壓縮的 24 位元樣本 (Q0.23 格式) |
AAUDIO_FORMAT_PCM_I32 | int32_t | 常用的 32 位元樣本 (Q0.31 格式) |
AAUDIO_FORMAT_IEC61937 | uint8_t | 以 IEC61937 封裝的壓縮音訊,適用於 HDMI 或 S/PDIF 直通機制 |
如果您要求特定取樣格式,則即使該格式並非最適合裝置,串流也會使用該格式。如果未指定取樣格式,AAudio 會選擇最適合的格式。 開啟串流後,您必須查詢取樣資料格式,並視情況轉換資料,如以下範例所示:
aaudio_format_t dataFormat = AAudioStream_getDataFormat(stream);
//... later
if (dataFormat == AAUDIO_FORMAT_PCM_I16) {
convertFloatToPcm16(...)
}
建立音訊串流
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
,藉此指定最佳值。為了安全起見,請在建立音訊串流後檢查狀態,如下方步驟 4 所述。
- 設定 AAudioStreamBuilder 後,請使用 AAudioStreamBuilder 建立串流:
AAudioStream *stream; result = AAudioStreamBuilder_openStream(builder, &stream);
- 建立串流後,請驗證其設定。如果您已指定取樣格式、取樣率或每個影格的取樣數,則這些設定將維持不變。 如果您已指定共用模式或緩衝區容量,這些設定可能會變更,具體取決於串流音訊裝置的功能和執行串流的 Android 裝置。做為良好的防禦性程式設計的一環,建議您先檢查串流設定,然後再使用。 您可採用函式擷取對應各個建構工具設定的串流設定:
- 您可以儲存建構工具供日後重複使用,建立更多串流。但如果您不打算再使用建構工具,建議您將其刪除。
AAudioStreamBuilder_delete(builder);
使用音訊串流
狀態轉換
AAudio 串流通常處於以下其中一種穩定狀態 (本節結尾將介紹錯誤狀態「已中斷連線」):
- 開啟
- 已開始
- 已暫停
- 已清除
- 已停止
只有在串流處於「已開始」狀態時,資料才會透過串流傳輸。如要轉換串流的狀態,請使用以下其中一個函式來要求轉換狀態:
aaudio_result_t result;
result = AAudioStream_requestStart(stream);
result = AAudioStream_requestStop(stream);
result = AAudioStream_requestPause(stream);
result = AAudioStream_requestFlush(stream);
請注意,您只能要求暫停或清除輸出串流:
這些函式屬於非同步函式,而且狀態不會立即變更。當您要求狀態變更時,串流會移動其中一個對應的暫時狀態:
- 正在開始
- 正在暫停
- 正在清除
- 正在停止
- 正在關閉
下列狀態圖中的圓角矩形代表穩定狀態,虛線矩形代表暫時狀態。儘管 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()
。
讀取及寫入音訊串流
串流開始後,可以透過兩種方式處理當中的資料:
- 使用高優先順序回呼。
- 使用
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);
}
您可以先準備好串流的緩衝區,然後再透過寫入資料或靜音資料以開始串流。必須在 timeoutNanos 設為零的情況下使用非阻斷式呼叫,才能完成這項操作。
緩衝區中的資料必須與 AAudioStream_getDataFormat()
傳回的資料格式相符。
關閉音訊串流
使用完串流之後,請關閉串流:
AAudioStream_close(stream);
關閉串流後,就無法搭配任何以 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.
}
最佳化效能
您可以調整音訊應用程式的內部緩衝區,並使用特殊的高優先順序執行緒,盡可能提高音訊應用程式的效能。
調整緩衝區以最大程度縮短延遲時間
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);
}
}
}
使用這項方法來最佳化輸入串流的緩衝區大小並無益處。 輸入串流會盡快執行,並嘗試將緩衝處理的資料量保持在最少量,然後在應用程式被先占時填滿緩衝區。
使用高優先順序回呼
如果應用程式從一般執行緒讀取音訊資料或將資料寫入其中,可能會被先占或發生定時抖動的情況,這會造成音訊故障。 使用較大型的緩衝區可能有助於避免出現這類故障,但如果緩衝區較大,音訊延遲時間也會較長。 如果是需要短延遲時間的應用程式,音訊串流可以使用非同步回呼函式,將資料傳輸至應用程式,或從中傳出資料。 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;
}
請注意,在本範例中,假設輸入和輸出串流的聲道數、格式和取樣率都相同。只要程式碼能正常處理轉換作業,串流格式不相符也無妨。
設定效能模式
每個 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 builder
AAudioStreamBuilder *streamBuilder;
AAudio_createStreamBuilder(&streamBuilder);
AAudioStreamBuilder_setDataCallback(streamBuilder, dataCallback, nullptr);
AAudioStreamBuilder_setPerformanceMode(streamBuilder, AAUDIO_PERFORMANCE_MODE_LOW_LATENCY);
// Use it to create the stream
AAudioStream *stream;
AAudioStreamBuilder_openStream(streamBuilder, &stream);
執行緒安全
AAudio API 無法完全確保執行緒安全。 您無法一次從多個執行緒並行呼叫部分 AAudio 函式,這是因為 AAudio 避免使用互斥鎖,而這可能會造成執行緒先占和故障。
為了安全起見,請勿從兩個不同的執行緒呼叫 AAudioStream_waitForStateChange()
或者讀取或寫入同一個串流。同樣地,在執行緒中關閉串流時,請勿從其他執行緒讀取或寫入串流。
傳回串流設定 (例如 AAudioStream_getSampleRate()
和 AAudioStream_getChannelCount()
) 的呼叫皆屬於執行緒安全的呼叫。
另外,以下呼叫也是執行緒安全的呼叫:
AAudio_convert*ToText()
AAudio_createStreamBuilder()
AAudioStream_get*()
(AAudioStream_getTimestamp()
除外)
已知問題
- 由於 Android O DP2 版本並未使用快速音軌,因此阻塞式 write() 的音訊延遲時間較長,建議您使用回呼來縮短延遲時間。
其他資源
如需瞭解更多資訊,不妨參考以下資源: