AAudio

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。

  1. 建立 AAudioStreamBuilder:

    AAudioStreamBuilder *builder;
    aaudio_result_t result = AAudio_createStreamBuilder(&builder);
    

  2. 使用與串流參數對應的建構工具函式,設置建構工具中的音訊串流設定。您可以使用下列選用設定函式:

    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 所述。

  3. 設定 AAudioStreamBuilder 後,請使用 AAudioStreamBuilder 建立串流:

    AAudioStream *stream;
    result = AAudioStreamBuilder_openStream(builder, &stream);
    

  4. 建立串流後,請驗證其設定。如果您已指定取樣格式、取樣率或每個影格的取樣數,則這些設定將維持不變。 如果您已指定共用模式或緩衝區容量,這些設定可能會變更,具體取決於串流音訊裝置的功能和執行串流的 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()

  5. 您可以儲存建構工具供日後重複使用,建立更多串流。但如果您不打算再使用建構工具,建議您將其刪除。

    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 生命週期

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()

讀取及寫入音訊串流

串流開始後,可以透過兩種方式處理當中的資料:

如要進行傳輸指定影格數的封阻式讀取或寫入作業,請將 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 的緩衝區大小是所回報突發讀寫大小的倍數時,延遲時間最短。

      AAudio 緩衝處理中

為緩衝區大小進行最佳化的方法之一,就是先從大型緩衝區開始,逐漸將其減少直到開始出現緩衝區不足情形時,再稍微將其調大。或者,您可以先從小型緩衝區開始,如果出現緩衝區不足情形,則增加緩衝區大小,直到輸出流再次順暢為止。

這項程序進行的速度很快,可能會在使用者開始播放第一個音訊之前就已完成。建議您在執行初始緩衝區大小調整時先採用靜音,這樣使用者才不會聽到任何音訊干擾聲。系統效能可能會隨時間改變 (例如使用者可能會關閉飛航模式)。由於緩衝區調整僅會增加少許負荷 就能在應用程式讀取資料或將資料寫入串流時持續作業。

以下是緩衝區最佳化迴圈的範例:

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() 的音訊延遲時間較長,建議您使用回呼來縮短延遲時間。

其他資源

如需瞭解更多資訊,不妨參考以下資源:

API 參考資料

程式碼研究室

影片