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 用於 HDMI 或 S/PDIF 直通的 IEC61937 壓縮音訊

如果您要求的是特定的取樣格式,則即使該格式不是最適合裝置的選項,串流也會使用該格式。 如果未指定取樣格式,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 設為 0 的情況下,透過非阻塞式呼叫完成。

緩衝區中的資料必須與 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 參考資料

程式碼研究室

影片