獨佔模式數據流
如先前所述,如果應用程式以獨佔模式開啟數據流,則應用程式會獨佔使用播放或錄製數據流的音訊端點裝置。 相反地,數個應用程式可以在裝置上開啟共用模式串流來共用音訊端點裝置。
對音訊裝置的獨佔模式存取可能會封鎖重要的系統音效、防止與其他應用程式的互操作性,否則會降低用戶體驗。 為了減輕這些問題,當應用程式不是前景進程或未主動串流時,具有獨佔模式數據流的應用程式通常會放棄音訊裝置的控制。
串流延遲是連接應用程式端點緩衝區與音訊端點裝置之數據路徑固有的延遲。 對於轉譯數據流,延遲是應用程式將範例寫入端點緩衝區到透過喇叭聽到範例的時間,延遲上限。 針對擷取串流,延遲是從聲音進入麥克風到應用程式可以從端點緩衝區讀取該聲音樣本的時間上限。
使用獨佔模式數據流的應用程式通常會這樣做,因為它們需要音訊端點裝置與存取端點緩衝區的應用程式線程之間的數據路徑低延遲。 一般而言,這些線程會以相對較高的優先順序執行,並排程本身以接近的定期間隔執行,或與分隔音訊硬體後續處理通過的定期間隔相同。 在每個傳遞期間,音訊硬體會處理端點緩衝區中的新數據。
為了達到最小的數據流延遲,應用程式可能需要特殊的音訊硬體,以及輕載的計算機系統。 將音訊硬體驅動超過其計時限制或載入具有競爭高優先順序工作的系統,可能會導致低延遲音訊串流發生問題。 例如,針對轉譯數據流,如果應用程式在音訊硬體讀取緩衝區之前無法寫入端點緩衝區,或硬體在排定播放緩衝區之前無法讀取緩衝區,就會發生問題。 一般而言,打算在各種不同的音訊硬體上執行的應用程式,而且在廣泛的系統中,應該放寬其計時需求,以避免在所有目標環境中發生問題。
Windows Vista 有數個功能可支援需要低延遲音訊串流的應用程式。 如使用者模式音訊元件中所述,執行時間關鍵作業的應用程式可以呼叫多媒體類別排程器服務 (MMCSS) 函式來增加線程優先順序,而不拒絕 CPU 資源至低優先順序的應用程式。 此外, IAudioClient::Initialize 方法支援一個AUDCLNT_STREAMFLAGS_EVENTCALLBACK旗標,可讓應用程式的緩衝區服務線程排程從音訊裝置取得新的緩衝區時執行。 藉由使用這些功能,應用程式線程可以降低執行時間的不確定性,進而降低低延遲音訊串流中發生問題的風險。
舊版音訊適配卡的驅動程式可能會使用 WaveCyclic 或 WavePci 設備驅動器介面 (DDI),而較新音訊適配卡的驅動程式更有可能支援 WaveRT DDI。 對於獨佔模式應用程式,WaveRT 驅動程式可以提供比 WaveCyclic 或 WavePci 驅動程式更好的效能,但 WaveRT 驅動程式需要額外的硬體功能。 這些功能包括直接與應用程式共用硬體緩衝區的能力。 透過直接共用,不需要系統介入,即可在獨佔模式應用程式與音訊硬體之間傳輸數據。 相反地,WaveCyclic 和 WavePci 驅動程式適用於較舊、功能較弱的音訊配接器。 這些適配卡依賴系統軟體在應用程式緩衝區與硬體緩衝區之間傳輸數據區塊(附加至系統 I/O 要求封包或 IRP)。 此外,USB 音訊裝置依賴系統軟體在應用程式緩衝區與硬體緩衝區之間傳輸數據。 為了改善連接至依賴系統進行數據傳輸之音訊裝置之獨佔模式應用程式的效能,WASAPI 會自動增加在應用程式和硬體之間傳輸數據的系統線程優先順序。 WASAPI 會使用 MMCSS 來增加線程優先順序。 在 Windows Vista 中,如果系統線程使用 PCM 格式管理獨佔模式音訊播放數據流的數據傳輸,且裝置期間少於 10 毫秒,WASAPI 會將 MMCSS 工作名稱 “Pro Audio” 指派給線程。 如果數據流的裝置期間大於或等於 10 毫秒,WASAPI 會將 MMCSS 工作名稱 “Audio” 指派給線程。 如需 WaveCyclic、WavePci 和 WaveRT DIS 的詳細資訊,請參閱 Windows DDK 檔。 如需選取適當裝置期間的相關信息,請參閱 IAudioClient::GetDevicePeriod。
如會話音量控制中所述,WASAPI 提供 ISimpleAudioVolume、IChannelAudioVolume 和 IAudioStreamVolume 介面,以控制共用模式音訊數據流的音量層級。 不過,這些介面中的控件對獨佔模式數據流沒有任何影響。 相反地,管理獨佔模式數據流的應用程式通常會使用 EndpointVolume API 中的 IAudioEndpointVolume 介面來控制這些數據流的磁碟區層級。 如需此介面的相關信息,請參閱 端點磁碟區控件。
針對系統中的每個播放裝置和擷取裝置,使用者可以控制裝置是否可用於獨佔模式。 如果使用者停用裝置的獨佔模式使用,則裝置可用來播放或只記錄共用模式串流。
如果使用者啟用裝置的獨佔模式使用,使用者也可以控制應用程式是否要求以獨佔模式使用裝置,將優先於目前可能透過裝置播放或錄製共用模式串流的應用程式使用裝置。 如果啟用先占功能,如果裝置目前未使用,或裝置正以共用模式使用裝置,則應用程式要求會成功,但如果另一個應用程式已擁有裝置的獨佔控制權,則要求會失敗。 如果先占已停用,如果裝置目前未使用,則應用程式要求以獨佔方式控制裝置會成功,但如果裝置已在共用模式或獨佔模式中使用,要求就會失敗。
在 Windows Vista 中,音訊端點裝置的預設設定如下:
- 裝置可用來播放或錄製獨佔模式串流。
- 使用裝置播放或錄製獨佔模式數據流的要求會先佔目前正在透過裝置播放或錄製的任何共用模式數據流。
變更播放或錄製裝置的獨佔模式設定
- 以滑鼠右鍵按兩下位於任務列右側的通知區域中的喇叭圖示,然後選取 [播放裝置] 或 [錄製裝置]。 (或者,從命令提示字元視窗執行 Windows 多媒體控制面板,Mmsys.cpl。如需詳細資訊,請參閱 DEVICE_STATE_XXX 常數中的備註。)
- [音效] 視窗出現之後,選取 [播放] 或 [錄製]。 接下來,選取裝置名稱清單中的項目,然後按下 [ 屬性]。
- 在 [屬性] 視窗出現之後,按兩下 [ 進階]。
- 若要讓應用程式以獨佔模式使用裝置,請核取標示 為 [允許應用程式完全控制此裝置] 的方塊。 若要停用裝置的獨佔模式使用,請清除複選框。
- 如果已啟用裝置的獨佔模式使用,您可以指定當裝置目前正在播放或錄製共用模式串流時,裝置的獨佔控制要求是否成功。 若要讓獨佔模式應用程式優先於共用模式應用程式,請核取標示為 [授與獨佔模式應用程式優先順序] 的方塊。 若要拒絕獨佔模式應用程式優先於共用模式應用程式,請清除複選框。
下列程式代碼範例示範如何在設定為在獨佔模式使用的音訊轉譯裝置上播放低延遲音訊串流:
//-----------------------------------------------------------
// Play an exclusive-mode stream on the default audio
// rendering device. The PlayExclusiveStream function uses
// event-driven buffering and MMCSS to play the stream at
// the minimum latency supported by the device.
//-----------------------------------------------------------
// REFERENCE_TIME time units per second and per millisecond
#define REFTIMES_PER_SEC 10000000
#define REFTIMES_PER_MILLISEC 10000
#define EXIT_ON_ERROR(hres) \
if (FAILED(hres)) { goto Exit; }
#define SAFE_RELEASE(punk) \
if ((punk) != NULL) \
{ (punk)->Release(); (punk) = NULL; }
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);
HRESULT PlayExclusiveStream(MyAudioSource *pMySource)
{
HRESULT hr;
REFERENCE_TIME hnsRequestedDuration = 0;
IMMDeviceEnumerator *pEnumerator = NULL;
IMMDevice *pDevice = NULL;
IAudioClient *pAudioClient = NULL;
IAudioRenderClient *pRenderClient = NULL;
WAVEFORMATEX *pwfx = NULL;
HANDLE hEvent = NULL;
HANDLE hTask = NULL;
UINT32 bufferFrameCount;
BYTE *pData;
DWORD flags = 0;
DWORD taskIndex = 0;
hr = CoCreateInstance(
CLSID_MMDeviceEnumerator, NULL,
CLSCTX_ALL, IID_IMMDeviceEnumerator,
(void**)&pEnumerator);
EXIT_ON_ERROR(hr)
hr = pEnumerator->GetDefaultAudioEndpoint(
eRender, eConsole, &pDevice);
EXIT_ON_ERROR(hr)
hr = pDevice->Activate(
IID_IAudioClient, CLSCTX_ALL,
NULL, (void**)&pAudioClient);
EXIT_ON_ERROR(hr)
// Call a helper function to negotiate with the audio
// device for an exclusive-mode stream format.
hr = GetStreamFormat(pAudioClient, &pwfx);
EXIT_ON_ERROR(hr)
// Initialize the stream to play at the minimum latency.
hr = pAudioClient->GetDevicePeriod(NULL, &hnsRequestedDuration);
EXIT_ON_ERROR(hr)
hr = pAudioClient->Initialize(
AUDCLNT_SHAREMODE_EXCLUSIVE,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
hnsRequestedDuration,
hnsRequestedDuration,
pwfx,
NULL);
if (hr == AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED) {
// Align the buffer if needed, see IAudioClient::Initialize() documentation
UINT32 nFrames = 0;
hr = pAudioClient->GetBufferSize(&nFrames);
EXIT_ON_ERROR(hr)
hnsRequestedDuration = (REFERENCE_TIME)((double)REFTIMES_PER_SEC / pwfx->nSamplesPerSec * nFrames + 0.5);
hr = pAudioClient->Initialize(
AUDCLNT_SHAREMODE_EXCLUSIVE,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
hnsRequestedDuration,
hnsRequestedDuration,
pwfx,
NULL);
}
EXIT_ON_ERROR(hr)
// Tell the audio source which format to use.
hr = pMySource->SetFormat(pwfx);
EXIT_ON_ERROR(hr)
// Create an event handle and register it for
// buffer-event notifications.
hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
if (hEvent == NULL)
{
hr = E_FAIL;
goto Exit;
}
hr = pAudioClient->SetEventHandle(hEvent);
EXIT_ON_ERROR(hr);
// Get the actual size of the two allocated buffers.
hr = pAudioClient->GetBufferSize(&bufferFrameCount);
EXIT_ON_ERROR(hr)
hr = pAudioClient->GetService(
IID_IAudioRenderClient,
(void**)&pRenderClient);
EXIT_ON_ERROR(hr)
// To reduce latency, load the first buffer with data
// from the audio source before starting the stream.
hr = pRenderClient->GetBuffer(bufferFrameCount, &pData);
EXIT_ON_ERROR(hr)
hr = pMySource->LoadData(bufferFrameCount, pData, &flags);
EXIT_ON_ERROR(hr)
hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags);
EXIT_ON_ERROR(hr)
// Ask MMCSS to temporarily boost the thread priority
// to reduce glitches while the low-latency stream plays.
hTask = AvSetMmThreadCharacteristics(TEXT("Pro Audio"), &taskIndex);
if (hTask == NULL)
{
hr = E_FAIL;
EXIT_ON_ERROR(hr)
}
hr = pAudioClient->Start(); // Start playing.
EXIT_ON_ERROR(hr)
// Each loop fills one of the two buffers.
while (flags != AUDCLNT_BUFFERFLAGS_SILENT)
{
// Wait for next buffer event to be signaled.
DWORD retval = WaitForSingleObject(hEvent, 2000);
if (retval != WAIT_OBJECT_0)
{
// Event handle timed out after a 2-second wait.
pAudioClient->Stop();
hr = ERROR_TIMEOUT;
goto Exit;
}
// Grab the next empty buffer from the audio device.
hr = pRenderClient->GetBuffer(bufferFrameCount, &pData);
EXIT_ON_ERROR(hr)
// Load the buffer with data from the audio source.
hr = pMySource->LoadData(bufferFrameCount, pData, &flags);
EXIT_ON_ERROR(hr)
hr = pRenderClient->ReleaseBuffer(bufferFrameCount, flags);
EXIT_ON_ERROR(hr)
}
// Wait for the last buffer to play before stopping.
Sleep((DWORD)(hnsRequestedDuration/REFTIMES_PER_MILLISEC));
hr = pAudioClient->Stop(); // Stop playing.
EXIT_ON_ERROR(hr)
Exit:
if (hEvent != NULL)
{
CloseHandle(hEvent);
}
if (hTask != NULL)
{
AvRevertMmThreadCharacteristics(hTask);
}
CoTaskMemFree(pwfx);
SAFE_RELEASE(pEnumerator)
SAFE_RELEASE(pDevice)
SAFE_RELEASE(pAudioClient)
SAFE_RELEASE(pRenderClient)
return hr;
}
在上述程式代碼範例中,PlayExclusiveStream 函式會在正在播放轉譯數據流時,在服務端點緩衝區的應用程式線程中執行。 函式會採用單一參數 pMySource,這是屬於用戶端定義類別 MyAudioSource 之物件的指標。 這個類別有兩個成員函式 LoadData 和 SetFormat,在程式碼範例中呼叫。 MyAudioSource 會在轉譯數據流中描述。
PlayExclusiveStream 函式會呼叫協助程式函式 GetStreamFormat,該函式會與預設轉譯裝置交涉,以判斷裝置是否支持適合應用程式使用的獨佔模式數據流格式。 GetStreamFormat 函式的程式代碼不會出現在程式代碼範例中;這是因為其實作的詳細數據完全取決於應用程式的需求。 不過,您可以簡單地描述 GetStreamFormat 函式的作業—它會呼叫 IAudioClient::IsFormatSupported 方法一或多次,以判斷裝置是否支持適當的格式。 應用程式的需求會決定 GetStreamFormat 向 IsFormatSupported 方法呈現的格式,以及其呈現順序。 如需IsFormatSupported的詳細資訊,請參閱裝置格式。
在 GetStreamFormat 呼叫之後,PlayExclusiveStream 函式會呼叫 IAudioClient::GetDevicePeriod 方法,以取得音訊硬體支援的最小裝置期間。 接下來,函式會呼叫 IAudioClient::Initialize 方法,要求緩衝區持續時間等於最小期間。 如果呼叫成功, Initialize 方法會配置兩個端點緩衝區,每個緩衝區在持續時間內等於最小期間。 稍後,當音訊串流開始執行時,應用程式和音訊硬體會以「乒乓球」方式共用兩個緩衝區,也就是說,當應用程式寫入一個緩衝區時,硬體會從另一個緩衝區讀取。
啟動資料流之前,PlayExclusiveStream 函式會執行下列動作:
- 建立並註冊事件句柄,以在緩衝區準備好填滿時接收通知。
- 將第一個緩衝區填入來自音訊來源的數據,以減少從數據流開始執行到聽到初始音效時的延遲。
- 呼叫 AvSetMmThreadCharacteristics 函式,要求 MMCSS 增加 PlayExclusiveStream 執行所在的線程優先順序。 (當數據流停止執行時, AvRevertMmThreadCharacteristics 函式呼叫會還原原始線程優先順序。
如需 AvSetMmThreadCharacteristics 和 AvRevertMmThreadCharacteristics 的詳細資訊,請參閱 Windows SDK 檔。
當數據流執行時,上述程式代碼範例中 while 迴圈的每個反覆專案都會填入一個端點緩衝區。 在 反覆項目之間,WaitForSingleObject 函式呼叫會等候事件句柄發出訊號。 當句柄收到訊號時,迴圈主體會執行下列動作:
- 呼叫 IAudioRenderClient::GetBuffer 方法來取得下一個緩衝區。
- 填入緩衝區。
- 呼叫 IAudioRenderClient::ReleaseBuffer 方法來釋放緩衝區。
如需 WaitForSingleObject 的詳細資訊,請參閱 Windows SDK 檔。
如果音訊配接器是由 WaveRT 驅動程式所控制,事件句柄的訊號會系結至來自音訊硬體的 DMA 傳輸通知。 若為 USB 音訊裝置,或由 WaveCyclic 或 WavePci 驅動程式控制的音訊裝置,事件句柄的訊號會系結至從應用程式緩衝區傳輸數據到硬體緩衝區的 IRP 完成。
上述程式代碼範例會將音訊硬體和計算機系統推送至其效能限制。 首先,為了減少串流延遲,應用程式會排程其緩衝區服務線程,以使用音訊硬體將支援的最低裝置期間。 其次,為了確保線程能可靠地在每個裝置期間內執行, AvSetMmThreadCharacteristics 函式呼叫會將TaskName參數設定為 “Pro Audio”,也就是在 Windows Vista 中,預設工作名稱的優先順序最高。 請考慮應用程式的時間需求是否可能放寬,而不會損害其實用性。 例如,應用程式可能會排程其緩衝區服務線程,以使用長度超過最小值的期間。 較長的期間可能會安全地允許使用較低的線程優先順序。
相關主題