本文件說明使用音訊時要考慮的重要做法,並示範 Marble Maze 如何套用這些做法。 Marble Maze 會使用 Microsoft Media Foundation 來載入檔案的音訊資源, 而 XAudio2 則用來混合播放音訊,以及將效果套用至音訊。
Marble Maze 在背景播放音樂,也使用遊戲音效來表示遊戲事件,例如彈珠撞牆時。 實作的一個重要部分是 Marble Maze 會在彈跳時使用回聲或回聲效果來模擬彈珠的聲音。 殘響效果的實施使得回音在小房間中可以更快且更響亮地傳到您附近;而在較大的房間中,回音比較安靜,且到達您身邊的速度較慢。
備註
對應至此檔的範例程式代碼位於 DirectX Marble Maze 遊戲範例中找到。
以下是本文件中討論在遊戲中使用音訊時的重要要點:
請考慮使用媒體基礎來解碼音訊資產,並使用 XAudio2 來播放音訊。 不過,如果您有可在通用 Windows 平臺 (UWP) 應用程式中運作的現有音訊資產載入機制,您可以使用它。
音訊圖包含每個作用中聲音的一個來源語音、零或多個副混音語音,以及一個主控語音。 來源語音可以饋送至副混音和/或主控語音。 副混音會饋送至其他副混音或主控語音。
如果您的背景音樂檔案很大,請考慮將您的音樂串流到較小的緩衝區,以便使用較少的記憶體。
如果有必要,當應用程式失去焦點、可見度或被暫停時,暫停音訊播放。 在應用程式重新取得焦點、變得可見或恢復運行時,繼續播放。
設定音訊類別以反映每個音效的角色。 例如,您通常會針對遊戲背景音訊使用 AudioCategory_GameMedia ,並針對音效使用 AudioCategory_GameEffects 。
釋放並重新建立所有音訊資源和介面,以處理裝置變更,包括耳機。
請考慮在將磁碟空間和串流成本降到最低時,是否要壓縮音訊檔案。 否則,您可以將音訊保持未壓縮,使其載入速度更快。
XAudio2 和 Microsoft Media Foundation 簡介
XAudio2 是 Windows 低階音訊庫,特別支援遊戲音訊。 它為遊戲提供數位訊號處理(DSP)和音訊圖形引擎。 XAudio2 藉由支援 SIMD 浮點架構和 HD 音訊等運算趨勢,擴充其前身 DirectSound 和 XAudio。 它也支援當今遊戲更複雜的音效處理需求。
XAudio2 重要概念文件說明使用 XAudio2 的重要概念。 簡言之,概念如下:
IXAudio2 介面是 XAudio2 引擎的核心。 Marble Maze 會使用此介面來建立語音,並在輸出裝置變更或失敗時接收通知。
語音 處理、調整及播放音訊數據。
來源語音是音訊通道的集合(單聲道、5.1 等等),代表一個音訊數據流。 在 XAudio2 中,來源語音是音訊處理開始的地方。 一般而言,聲音數據會從外部來源載入,例如檔案或網路,並傳送至來源語音。 Marble Maze 使用 媒體基礎 從檔案載入聲音數據。 本文件稍後會介紹Media Foundation。
副混音 處理音頻資料。 此處理可能包括變更音訊數據流,或將多個數據流合併成一個數據流。 Marble Maze 使用副混音來建立殘響效果。
主控語音 結合來自來源和副混音語音的數據,並將該數據傳送至音訊硬體。
音訊圖形 包含每個作用中聲音的一個來源聲音、零或多個副混音語音,以及只有一個主控語音。
回呼 通知客戶端程式碼某個事件已發生在語音或引擎對象中。 藉由使用回呼,您可以在 XAudio2 完成緩衝區時重複使用記憶體、當音訊裝置變更時做出反應(例如,當您連接或中斷耳機連線時),等等。 稍後在本檔案中,章節 將說明 Marble Maze 如何利用這個機制來處理耳機和裝置變更(
)。
Marble Maze 使用兩個音訊引擎(換句話說,兩個 IXAudio2 物件)來處理音訊。 一個引擎會處理背景音樂,另一個引擎會處理遊戲音效。
Marble Maze 也必須為每個引擎建立一個主控語音。 回想一下,主控引擎會將音訊串流合併成一個數據流,並將該數據流傳送至音訊硬體。 背景音樂串流,來源語音,將數據輸出到主控語音和兩個副混音語音。 副混音會執行殘響效果。
Media Foundation 是支援許多音訊和視訊格式的多媒體媒體櫃。 XAudio2 和 Media Foundation 彼此互補。 Marble Maze 使用媒體基礎從檔案載入音訊資產,並使用 XAudio2 播放音訊。 您不需要使用Media Foundation媒體基礎架構來載入音訊素材。 如果您有可在通用 Windows 平臺 (UWP) 應用程式中運作的現有音訊資產載入機制,請使用它。 音訊、視訊和相機 討論在UWP應用程式中實作音訊的幾種方式。
如需 XAudio2 的詳細資訊,請參閱 程式設計指南。 如需了解有關 Microsoft Media Foundation 的詳細資訊,請參閱 Microsoft Media Foundation。
初始化音訊資源
Marble Mazes 會針對背景音樂使用 Windows Media Audio (.wma) 檔案,以及用於遊戲音效的 WAV (.wav) 檔案。 這些格式由 Media Foundation 支援。 雖然 XAudio2 原生支援.wav檔案格式,但遊戲必須手動剖析檔格式,才能填寫適當的 XAudio2 數據結構。 Marble Maze 使用 Media Foundation,更輕鬆地處理 .wav 檔案。 如需媒體基礎所支援媒體格式的完整清單,請參閱 媒體基礎中支援的媒體格式。 Marble Maze 不使用個別的設計階段和運行階段的音訊格式,而且不使用 XAudio2 ADPCM 壓縮支援功能。 如需 XAudio2 中 ADPCM 壓縮的詳細資訊,請參閱 ADPCM 概觀。
從 MarbleMazeMain::LoadDeferredResources呼叫的 Audio::CreateResources 方法,用於載入檔案中的音訊流、初始化 XAudio2 引擎物件,以及創建來源、副總混合及主控語音。
建立 XAudio2 引擎
回想一下,Marble Maze 會建立一個 IXAudio2 對象來代表它所使用的每個音訊引擎。 若要建立音訊引擎,請呼叫 XAudio2Create 方法。 下列範例示範 Marble Maze 如何建立處理背景音樂的音訊引擎。
// In Audio.h
class Audio
{
private:
IXAudio2* m_musicEngine;
// ...
}
// In Audio.cpp
void Audio::CreateResources()
{
try
{
// ...
DX::ThrowIfFailed(
XAudio2Create(&m_musicEngine)
);
// ...
}
// ...
}
Marble Maze 會執行類似的步驟,以建立播放遊戲音效的音訊引擎。
如何在 UWP 應用程式中使用 IXAudio2 介面與桌面應用程式不同,主要體現在兩個方面。 首先,您不需要在呼叫 XAudio2Create之前呼叫 CoInitializeEx。 此外, IXAudio2 不再支援裝置列舉。 如需如何列舉音訊裝置的資訊,請參閱 列舉裝置。
建立主控語音
以下範例展示如何透過 Audio::CreateResources 方法,使用 IXAudio2::CreateMasteringVoice 方法為背景音樂創建主控聲音。 在此範例中, m_musicMasteringVoice 是 IXAudio2MasteringVoice 物件。 我們指定兩個輸入通道:這會簡化殘響效果的邏輯。
我們會將 48000 指定為輸入取樣率。 我們選擇此取樣率,因為它代表音訊品質與所需CPU處理數量的平衡。 更高的取樣率需要更多的CPU處理,而不會有明顯的質量優點。
最後,我們會將 AudioCategory_GameMedia 指定為音訊串流類別,讓使用者在玩遊戲時可以接聽不同應用程式的音樂。 播放音樂應用程式時,Windows 會將 AudioCategory_GameMedia 選項所建立的任何語音設為靜音。 使用者仍然會聽到遊戲音效,因為它們是由 [AudioCategory_GameEffects ] 選項所建立。 如需音訊類別的詳細資訊,請參閱 AUDIO_STREAM_CATEGORY。
// This sample plays the equivalent of background music, which we tag on the
// mastering voice as AudioCategory_GameMedia. In ordinary usage, if we were
// playing the music track with no effects, we could route it entirely through
// Media Foundation. Here, we are using XAudio2 to apply a reverb effect to the
// music, so we use Media Foundation to decode the data then we feed it through
// the XAudio2 pipeline as a separate Mastering Voice, so that we can tag it
// as Game Media. We default the mastering voice to 2 channels to simplify
// the reverb logic.
DX::ThrowIfFailed(
m_musicEngine->CreateMasteringVoice(
&m_musicMasteringVoice,
2,
48000,
0,
nullptr,
nullptr,
AudioCategory_GameMedia
)
);
Audio::CreateResources 方法執行了類似的步驟,為遊戲音效創建主控聲音,不同之處在於指定了 AudioCategory_GameEffects 作為 StreamCategory 參數,這是 AudioCategory_GameEffects 的預設值。
建立殘響效果
針對每個語音,您可以使用 XAudio2 來建立處理音訊的效果序列。 這類序列稱為效果鏈結。 當您想要將一或多個效果套用至語音時,請使用效果鏈結。 效果鏈結可能是破壞性的;也就是說,鏈結中的每個效果都可以覆寫音頻緩衝區。 此屬性很重要,因為 XAudio2 不保證輸出緩衝區是以無聲初始化。 效果物件是由跨平台音訊處理物件 (XAPO) 在 XAudio2 中表示。 如需 XAPO 的詳細資訊,請參閱 XAPO 概觀。
當您建立效果鏈結時,請遵循下列步驟:
建立效果物件。
使用效果數據填充 XAUDIO2_EFFECT_DESCRIPTOR 的結構。
使用資料填充 XAUDIO2_EFFECT_CHAIN 結構。
將效果鏈結套用至語音。
填入特效參數結構,並將其套用至該特效。
視情況停用或啟用效果。
Audio 類別會定義 CreateReverb 方法來建立用於實作殘響的效果鏈結。 這個方法會呼叫 XAudio2CreateReverb 方法來建立 ComPtr<IUnknown> 物件,soundEffectXAPO,做為殘響效果的副混音語音。
Microsoft::WRL::ComPtr<IUnknown> soundEffectXAPO;
DX::ThrowIfFailed(
XAudio2CreateReverb(&soundEffectXAPO)
);
XAUDIO2_EFFECT_DESCRIPTOR 結構包含用於效果鏈結的 XAPO 資訊,例如目標輸出通道數。 Audio::CreateReverb 方法會建立 XAUDIO2_EFFECT_DESCRIPTOR 物件,soundEffectdescriptor,該物件設定為停用狀態、使用兩個輸出通道,以及針對殘響效果參考 soundEffectXAPO。 soundEffectdescriptor 會以停用狀態啟動,因為遊戲必須在效果開始修改遊戲音效之前設定參數。 Marble Maze 使用兩個輸出通道來簡化殘響效果的邏輯。
soundEffectdescriptor.InitialState = false;
soundEffectdescriptor.OutputChannels = 2;
soundEffectdescriptor.pEffect = soundEffectXAPO.Get();
如果您的效果鏈結有多個效果,則每個效果都需要物件。 XAUDIO2_EFFECT_CHAIN 結構會保存參與效果之 XAUDIO2_EFFECT_DESCRIPTOR 對象的陣列。 下列範例示範 Audio::CreateReverb 方法如何指定要實作殘響的一個效果。
XAUDIO2_EFFECT_CHAIN soundEffectChain;
// ...
soundEffectChain.EffectCount = 1;
soundEffectChain.pEffectDescriptors = &soundEffectdescriptor;
Audio::CreateReverb 方法會呼叫 IXAudio2::CreateSubmixVoice 方法來為該效果建立副混音聲音。 它會指定 pEffectChain 參數的 XAUDIO2_EFFECT_CHAIN 物件 soundEffectChain,以將效果鏈結與聲音產生關聯。 Marble Maze 也指定兩個輸出通道和 48 千赫茨的取樣率。
DX::ThrowIfFailed(
engine->CreateSubmixVoice(newSubmix, 2, 48000, 0, 0, nullptr, &soundEffectChain)
);
小提示
如果您想將現有的效果鏈附加到現有的子混音聲音,或者想要替換當前的效果鏈,請使用 IXAudio2Voice::SetEffectChain 方法。
Audio::CreateReverb 方法會呼叫 IXAudio2Voice::SetEffectParameters 來設定與效果相關聯的其他參數。 這個方法會採用效果特有的參數結構。 XAUDIO2FX_REVERB_PARAMETERS 物件 m_reverbParametersSmall,其中包含殘響的效果參數,會在 Audio::Initialize 方法中初始化,因為每個殘響效果都會共用相同的參數。 下列範例示範 Audio::Initialize 方法如何初始化近距離殘響的殘響參數。
m_reverbParametersSmall.ReflectionsDelay = XAUDIO2FX_REVERB_DEFAULT_REFLECTIONS_DELAY;
m_reverbParametersSmall.ReverbDelay = XAUDIO2FX_REVERB_DEFAULT_REVERB_DELAY;
m_reverbParametersSmall.RearDelay = XAUDIO2FX_REVERB_DEFAULT_REAR_DELAY;
m_reverbParametersSmall.PositionLeft = XAUDIO2FX_REVERB_DEFAULT_POSITION;
m_reverbParametersSmall.PositionRight = XAUDIO2FX_REVERB_DEFAULT_POSITION;
m_reverbParametersSmall.PositionMatrixLeft = XAUDIO2FX_REVERB_DEFAULT_POSITION_MATRIX;
m_reverbParametersSmall.PositionMatrixRight = XAUDIO2FX_REVERB_DEFAULT_POSITION_MATRIX;
m_reverbParametersSmall.EarlyDiffusion = 4;
m_reverbParametersSmall.LateDiffusion = 15;
m_reverbParametersSmall.LowEQGain = XAUDIO2FX_REVERB_DEFAULT_LOW_EQ_GAIN;
m_reverbParametersSmall.LowEQCutoff = XAUDIO2FX_REVERB_DEFAULT_LOW_EQ_CUTOFF;
m_reverbParametersSmall.HighEQGain = XAUDIO2FX_REVERB_DEFAULT_HIGH_EQ_GAIN;
m_reverbParametersSmall.HighEQCutoff = XAUDIO2FX_REVERB_DEFAULT_HIGH_EQ_CUTOFF;
m_reverbParametersSmall.RoomFilterFreq = XAUDIO2FX_REVERB_DEFAULT_ROOM_FILTER_FREQ;
m_reverbParametersSmall.RoomFilterMain = XAUDIO2FX_REVERB_DEFAULT_ROOM_FILTER_MAIN;
m_reverbParametersSmall.RoomFilterHF = XAUDIO2FX_REVERB_DEFAULT_ROOM_FILTER_HF;
m_reverbParametersSmall.ReflectionsGain = XAUDIO2FX_REVERB_DEFAULT_REFLECTIONS_GAIN;
m_reverbParametersSmall.ReverbGain = XAUDIO2FX_REVERB_DEFAULT_REVERB_GAIN;
m_reverbParametersSmall.DecayTime = XAUDIO2FX_REVERB_DEFAULT_DECAY_TIME;
m_reverbParametersSmall.Density = XAUDIO2FX_REVERB_DEFAULT_DENSITY;
m_reverbParametersSmall.RoomSize = XAUDIO2FX_REVERB_DEFAULT_ROOM_SIZE;
m_reverbParametersSmall.WetDryMix = XAUDIO2FX_REVERB_DEFAULT_WET_DRY_MIX;
m_reverbParametersSmall.DisableLateField = TRUE;
這個範例會使用大部分殘響參數的預設值,但會將 DisableLateField 設為 TRUE 以指定近場殘響,將 EarlyDiffusion 設為 4 以模擬平坦的近距表面,並將 LateDiffusion 設為 15 以模擬非常擴散的遠距表面。 接近的平坦表面使回聲更快、更響亮地傳到你;遙遠且分散的表面使回聲更安靜,並更慢地傳到你。 您可以實驗殘響值,以取得遊戲中所需的效果,或使用 ReverbConvertI3DL2ToNative 方法來使用業界標準的 I3DL2 (互動式 3D 音頻轉譯指導方針層級 2.0) 參數。
下列範例示範 Audio::CreateReverb 如何設定殘響參數。 newSubmix 是 IXAudio2SubmixVoice** 物件。 參數 是 XAUDIO2FX_REVERB_PARAMETERS* 物件。
DX::ThrowIfFailed(
(*newSubmix)->SetEffectParameters(0, parameters, sizeof(m_reverbParametersSmall))
);
Audio::CreateReverb 方法會在 enableEffect 旗標被設定時,利用 IXAudio2Voice::EnableEffect 來啟用效果,從而完成該過程。 它也會使用 IXAudio2Voice::SetVolume 來設定其音量,並使用 IXAudio2Voice::SetOutputMatrix來設定輸出矩陣。 這一部分會先將音量設定為最大(1.0),然後指定音量矩陣,使左聲道和右聲道的輸入和輸出喇叭均保持靜音。 我們這樣做是因為其他程式碼稍後會在兩個混響之間交錯淡化(模擬從靠近牆壁到位於大型房間的過渡),或視需要將兩個混響靜音。 當混響路徑稍後取消靜音時,遊戲會將矩陣 {1.0f、0.0f、0.0f、1.0f} 設定為將左混響輸出路由至總控聲道的左側輸入,並將右混響輸出路由至總控聲道的右側輸入。
if (enableEffect)
{
DX::ThrowIfFailed(
(*newSubmix)->EnableEffect(0)
);
}
DX::ThrowIfFailed(
(*newSubmix)->SetVolume (1.0f)
);
float outputMatrix[4] = {0, 0, 0, 0};
DX::ThrowIfFailed(
(*newSubmix)->SetOutputMatrix(masteringVoice, 2, 2, outputMatrix)
);
Marble Maze 呼叫 Audio::CreateReverb 方法四次:兩次用於背景音樂,兩次用於遊戲聲效。 下列顯示 Marble Maze 如何為背景音樂調用 CreateReverb 方法。
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersSmall,
&m_musicReverbVoiceSmallRoom,
true
);
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersLarge,
&m_musicReverbVoiceLargeRoom,
true
);
如需與 XAudio2 搭配使用之效果的可能來源清單,請參閱 XAudio2 音訊效果。
從檔案載入音訊數據
Marble Maze 定義了 MediaStreamer 類別,該類別使用 Media Foundation 從檔案載入音訊資源。 Marble Maze 使用一個 MediaStreamer 物件來載入每個音訊檔案。
Marble Maze 會呼叫 MediaStreamer::Initialize 方法來初始化每個音訊數據流。 以下是 Audio::CreateResources 方法如何呼叫 MediaStreamer::Initialize 來初始化背景音樂的音訊數據流:
// Media Foundation is a convenient way to get both file I/O and format decode for
// audio assets. You can replace the streamer in this sample with your own file I/O
// and decode routines.
m_musicStreamer.Initialize(L"Media\\Audio\\background.wma");
MediaStreamer::Initialize 方法會從呼叫 MFStartup 方法來初始化 Media Foundation 開始。 MF_VERSION 是在 mfapi.h中定義的巨集,我們用來指定要使用的 Media Foundation 版本。
DX::ThrowIfFailed(
MFStartup(MF_VERSION)
);
MediaStreamer::Initialize 接著會呼叫 MFCreateSourceReaderFromURL 來建立 IMFSourceReader 物件。 IMFSourceReader 物件m_reader會從URL所指定的檔案讀取媒體數據。
DX::ThrowIfFailed(
MFCreateSourceReaderFromURL(url, nullptr, &m_reader)
);
MediaStreamer::Initialize 方法接著會使用 MFCreateMediaType 來建立 IMFMediaType 物件,以描述音頻流格式。 音訊格式有兩種類型:主要類型和子類型。 主要類型會定義媒體的整體格式,例如視訊、音訊、腳本等等。 子類型會定義格式,例如 PCM、ADPCM 或 WMA。
MediaStreamer::Initialize 方法會使用 IMFAttributes::SetGUID 方法,將主要類型 (MF_MT_MAJOR_TYPE) 指定為音訊 (MFMediaType_Audio),並將次要類型 (MF_MT_SUBTYPE) 指定為未壓縮的 PCM 音訊 (MFAudioFormat_PCM)。 MF_MT_MAJOR_TYPE 和 MF_MT_SUBTYPE 是 媒體基礎屬性。 MFMediaType_Audio 和 MFAudioFormat_PCM 是類型和子類型 GUID;如需詳細資訊,請參閱 音訊媒體類型 。 IMFSourceReader::SetCurrentMediaType 方法會將媒體類型與數據流讀取器產生關聯。
// Set the decoded output format as PCM.
// XAudio2 on Windows can process PCM and ADPCM-encoded buffers.
// When this sample uses Media Foundation, it always decodes into PCM.
DX::ThrowIfFailed(
MFCreateMediaType(&mediaType)
);
DX::ThrowIfFailed(
mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)
);
DX::ThrowIfFailed(
mediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)
);
DX::ThrowIfFailed(
m_reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, mediaType.Get())
);
MediaStreamer::Initialize 方法接著會使用 IMFSourceReader::GetCurrentMediaType 從 Media Foundation 取得完整的輸出媒體格式,並呼叫 MFCreateWaveFormatExFromMFMediaType 方法,將 Media Foundation 音頻媒體類型轉換為WAVEATEX 結構。 WAVEFORMATEX 結構定義了波形音頻數據的格式。 Marble Maze 使用此結構來建立聲源,並將低通濾波器套用至彈珠滾動音效。
// Get the complete WAVEFORMAT from the Media Type.
DX::ThrowIfFailed(
m_reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &outputMediaType)
);
uint32 formatSize = 0;
WAVEFORMATEX* waveFormat;
DX::ThrowIfFailed(
MFCreateWaveFormatExFromMFMediaType(outputMediaType.Get(), &waveFormat, &formatSize)
);
CopyMemory(&m_waveFormat, waveFormat, sizeof(m_waveFormat));
CoTaskMemFree(waveFormat);
這很重要
MFCreateWaveFormatExFromMFMediaType 方法會使用 CoTaskMemAlloc 來配置 WAVEFORMATEX 物件。 因此,請務必在完成使用此物件時呼叫 CoTaskMemFree 。
MediaStreamer::Initialize 方法以計算數據流的長度(以位元組為單位,即 m_maxStreamLengthInBytes)來完成。 若要這樣做,它會呼叫 IMFSourceReader::GetPresentationAttribute 方法來取得 100 奈秒單位的音訊數據流持續時間,將持續時間轉換為區段,然後將平均數據傳輸速率乘以每秒位元組為單位。 Marble Maze 稍後會使用此值來配置保存每個遊戲音效的緩衝區。
// Get the total length of the stream, in bytes.
PROPVARIANT var;
DX::ThrowIfFailed(
m_reader->
GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, MF_PD_DURATION, &var)
);
// duration is in 100ns units; convert to seconds, and round up
// to the nearest whole byte.
ULONGLONG duration = var.uhVal.QuadPart;
m_maxStreamLengthInBytes =
static_cast<unsigned int>(
((duration * static_cast<ULONGLONG>(m_waveFormat.nAvgBytesPerSec)) + 10000000)
/ 10000000
);
建立原始語音
Marble Maze 會建立 XAudio2 來源音聲,以來源音聲播放其每個遊戲的音效和音樂。 Audio 類別會定義一個用於背景音樂的 IXAudio2SourceVoice 物件,以及一個用來保存遊戲音效的 SoundEffectData 物件陣列。 SoundEffectData 結構會保存效果的 IXAudio2SourceVoice 物件,也會定義其他效果相關數據,例如音頻緩衝區。 Audio.h 定義了 SoundEvent 列舉。 Marble Maze 會使用此列舉來識別每個遊戲音效。 Audio 類別也會使用此列舉來為 SoundEffectData 物件的陣列進行編列索引。
enum SoundEvent
{
RollingEvent = 0,
FallingEvent = 1,
CollisionEvent = 2,
CheckpointEvent = 3,
MenuChangeEvent = 4,
MenuSelectedEvent = 5,
LastSoundEvent,
};
下表顯示每個值之間的關聯性、包含相關聯音效數據的檔案,以及每個音效所代表內容的簡短描述。 音訊檔案位於 \Media\Audio 資料夾中。
| SoundEvent 值 | 檔案名稱 | 說明 |
|---|---|---|
| RollingEvent | MarbleRoll.wav | 隨著彈珠滾動而播放。 |
| 墜落事件 | MarbleFall.wav | 彈珠從迷宮中掉下來時播放。 |
| 碰撞事件 | MarbleHit.wav | 彈珠與迷宮相撞時播放。 |
| 檢查點事件 | Checkpoint.wav | 當彈珠經過檢查點時播放音效。 |
| 菜單變更事件 | MenuChange.wav | 當用戶變更目前的選單項目時播放。 |
| 菜單選擇事件 | MenuSelect.wav | 當用戶選取功能表項時播放。 |
下列範例示範 Audio::CreateResources 方法如何建立背景音樂的來源聲音。 XAUDIO2_SEND_DESCRIPTOR 結構會定義來自另一個語音的目標目的地語音,並指定是否應該使用篩選。 Marble Maze 會呼叫 Audio::SetSoundEffectFilter 方法,以在球滾動時使用濾波器來改變球的聲音。 XAUDIO2_VOICE_SENDS 結構用於定義一個聲音集,以接收來自單一輸出聲音的數據。 Marble Maze 將資料從來源聲音傳送到主控聲音(用於播放聲音中乾燥或未經修飾的部分),以及到實作濕或回響部分聲音的兩個子混音聲音。
IXAudio2::CreateSourceVoice 方法會建立並設定來源語音。 其會採用 WAVEFORMATEX 結構,以定義傳送至語音的音訊緩衝區的格式。 如先前所述,Marble Maze 會使用 PCM 格式。
XAUDIO2_SEND_DESCRIPTOR descriptors[3];
descriptors[0].pOutputVoice = m_musicMasteringVoice;
descriptors[0].Flags = 0;
descriptors[1].pOutputVoice = m_musicReverbVoiceSmallRoom;
descriptors[1].Flags = 0;
descriptors[2].pOutputVoice = m_musicReverbVoiceLargeRoom;
descriptors[2].Flags = 0;
XAUDIO2_VOICE_SENDS sends = {0};
sends.SendCount = 3;
sends.pSends = descriptors;
WAVEFORMATEX& waveFormat = m_musicStreamer.GetOutputWaveFormatEx();
DX::ThrowIfFailed(
m_musicEngine->CreateSourceVoice(&m_musicSourceVoice, &waveFormat, 0, 1.0f, &m_voiceContext, &sends, nullptr)
);
DX::ThrowIfFailed(
m_musicMasteringVoice->SetVolume(0.4f)
);
播放背景音樂
語音來源會在已停止狀態下建立。 Marble Maze 會在遊戲迴圈中啟動背景音樂。 第一次呼叫 MarbleMazeMain::Update 會呼叫 Audio::Start 來啟動背景音樂。
if (!m_audio.m_isAudioStarted)
{
m_audio.Start();
}
Audio::Start 方法會呼叫 IXAudio2SourceVoice::Start 開始處理背景音樂的來源聲音。
void Audio::Start()
{
if (m_engineExperiencedCriticalError)
{
return;
}
HRESULT hr = m_musicSourceVoice->Start(0);
if SUCCEEDED(hr) {
m_isAudioStarted = true;
}
else
{
m_engineExperiencedCriticalError = true;
}
}
來源語音會將音訊數據傳遞至音訊圖形的下一個階段。 在 Marble Maze 的範例中,下一個階段包含兩個子混音聲道,以套用兩個殘響效果於音訊。 一個副混音使用近場的晚期殘響,另一個使用遠場的晚期殘響。
每個子混音對最終聲音混合的貢獻量取決於房間的大小和形狀。 當球靠近牆壁或在小房間時,近場殘響的貢獻較多,而當球處於大空間時,遠場殘響的貢獻較多。 這項技術會產生更逼真的回音效果,當大理石穿過迷宮時。 若要深入瞭解 Marble Maze 如何實作此效果,請參閱 Marble Maze 原始碼中的 Audio::SetRoomSize 和 Physics::CalculateCurrentRoomSize 。
備註
在大部分房間大小相對相同的遊戲中,您可以使用更基本的殘響模型。 例如,您可以針對所有房間使用一個殘響設定,或者您可以為每個房間建立預先定義的殘響設定。
Audio::CreateResources 方法會使用媒體基礎架構來載入背景音樂。 不過,此時來源語音沒有要使用的音訊數據。 此外,由於背景音樂迴圈,來源聲音必須定期更新數據,讓音樂繼續播放。
為了保持來源音訊的數據完整,遊戲迴圈會在每一幀更新音頻緩衝區。 MarbleMazeMain::Render 方法會呼叫 Audio::Render 來處理背景音樂音訊緩衝區。 Audio 類別會定義三個音訊緩衝區的陣列,m_audioBuffers。 每個緩衝區都會保存 64 KB(65536 位元組)的數據。 迴圈會從 Media Foundation 物件讀取數據,並將該數據寫入來源語音,直到來源語音有三個佇列緩衝區為止。
謹慎
雖然 Marble Maze 使用 64 KB 的緩衝區來保存音樂數據,但您可能需要使用較大或較小的緩衝區。 此金額取決於您的遊戲需求。
// This sample processes audio buffers during the render cycle of the application.
// As long as the sample maintains a high-enough frame rate, this approach should
// not glitch audio. In game code, it is best for audio buffers to be processed
// on a separate thread that is not synced to the main render loop of the game.
void Audio::Render()
{
if (m_engineExperiencedCriticalError)
{
m_engineExperiencedCriticalError = false;
ReleaseResources();
Initialize();
CreateResources();
Start();
if (m_engineExperiencedCriticalError)
{
return;
}
}
try
{
bool streamComplete;
XAUDIO2_VOICE_STATE state;
uint32 bufferLength;
XAUDIO2_BUFFER buf = {0};
// Use MediaStreamer to stream the buffers.
m_musicSourceVoice->GetState(&state);
while (state.BuffersQueued <= MAX_BUFFER_COUNT - 1)
{
streamComplete = m_musicStreamer.GetNextBuffer(
m_audioBuffers[m_currentBuffer],
STREAMING_BUFFER_SIZE,
&bufferLength
);
if (bufferLength > 0)
{
buf.AudioBytes = bufferLength;
buf.pAudioData = m_audioBuffers[m_currentBuffer];
buf.Flags = (streamComplete) ? XAUDIO2_END_OF_STREAM : 0;
buf.pContext = 0;
DX::ThrowIfFailed(
m_musicSourceVoice->SubmitSourceBuffer(&buf)
);
m_currentBuffer++;
m_currentBuffer %= MAX_BUFFER_COUNT;
}
if (streamComplete)
{
// Loop the stream.
m_musicStreamer.Restart();
break;
}
m_musicSourceVoice->GetState(&state);
}
}
catch (...)
{
m_engineExperiencedCriticalError = true;
}
}
迴圈也會處理當 Media Foundation 物件到達流的結尾時的情況。 在此情況下,它會呼叫 IMFSourceReader::SetCurrentPosition 方法來重設音訊來源的位置。
void MediaStreamer::Restart()
{
if (m_reader == nullptr)
{
return;
}
PROPVARIANT var = {0};
var.vt = VT_I8;
DX::ThrowIfFailed(
m_reader->SetCurrentPosition(GUID_NULL, var)
);
}
若要實作單一緩衝區的音訊迴圈(或完全載入記憶體的整個音效),您可以在初始化音效時, 將 [XAUDIO2_BUFFER::LoopCount] 字段設定為 XAUDIO2_LOOP_INFINITE 。 Marble Maze 使用這項技術來播放玻璃珠的滾動音效。
if (sound == RollingEvent)
{
m_soundEffects[sound].m_audioBuffer.LoopCount = XAUDIO2_LOOP_INFINITE;
}
不過,針對背景音樂,Marble Maze 會直接管理緩衝區,以便更妥善地控制所使用的記憶體數量。 當您的音樂檔案很大時,您可以將音樂數據串流至較小的緩衝區。 這樣做有助於平衡記憶體大小與遊戲處理和串流音訊數據的頻率。
小提示
如果您的遊戲有低或波動的幀速率,在主線程上處理音訊可能會產生非預期的暫停或爆音,因為音訊引擎沒有足夠的音訊緩衝數據來處理。 如果您的遊戲對此問題很敏感,請考慮在未執行轉譯的個別線程上處理音訊。 這種方法在有多個處理器的計算機上特別有用,因為您的遊戲可以使用閑置處理器。
回應遊戲事件
Audio 類別提供方法如 PlaySoundEffect、IsSoundEffectStarted、StopSoundEffect、SetSoundEffectVolume、SetSoundEffectPitch 和 SetSoundEffectFilter,讓遊戲能夠控制音效的播放與停止,並且可以控制音量和音調等音效屬性。 例如,如果彈珠脫離迷宮,MarbleMazeMain::Update 呼叫 Audio::PlaySoundEffect 方法來播放 FallingEvent 音效。
m_audio.PlaySoundEffect(FallingEvent);
Audio::PlaySoundEffect 方法會呼叫 IXAudio2SourceVoice::Start 方法來開始播放音效。 如果已經呼叫 IXAudio2SourceVoice::Start 方法,則不會再次啟動。 Audio::PlaySoundEffect 然後執行用於特定音效的自定義邏輯。
void Audio::PlaySoundEffect(SoundEvent sound)
{
XAUDIO2_BUFFER buf = {0};
XAUDIO2_VOICE_STATE state = {0};
if (m_engineExperiencedCriticalError)
{
// If there's an error, then we'll recreate the engine on the next
// render pass.
return;
}
SoundEffectData* soundEffect = &m_soundEffects[sound];
HRESULT hr = soundEffect->m_soundEffectSourceVoice->Start();
if FAILED(hr)
{
m_engineExperiencedCriticalError = true;
return;
}
// For one-off voices, submit a new buffer if there's none queued up,
// and allow up to two collisions to be queued up.
if (sound != RollingEvent)
{
XAUDIO2_VOICE_STATE state = {0};
soundEffect->m_soundEffectSourceVoice->
GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED);
if (state.BuffersQueued == 0)
{
soundEffect->m_soundEffectSourceVoice->
SubmitSourceBuffer(&soundEffect->m_audioBuffer);
}
else if (state.BuffersQueued < 2 && sound == CollisionEvent)
{
soundEffect->m_soundEffectSourceVoice->
SubmitSourceBuffer(&soundEffect->m_audioBuffer);
}
// For the menu clicks, we want to stop the voice and replay the click
// right away.
// Note that stopping and then flushing could cause a glitch due to the
// waveform not being at a zero-crossing, but due to the nature of the
// sound (fast and 'clicky'), we don't mind.
if (state.BuffersQueued > 0 && sound == MenuChangeEvent)
{
soundEffect->m_soundEffectSourceVoice->Stop();
soundEffect->m_soundEffectSourceVoice->FlushSourceBuffers();
soundEffect->m_soundEffectSourceVoice->
SubmitSourceBuffer(&soundEffect->m_audioBuffer);
soundEffect->m_soundEffectSourceVoice->Start();
}
}
m_soundEffects[sound].m_soundEffectStarted = true;
}
對於滾動以外的音效,Audio::PlaySoundEffect 方法會呼叫 IXAudio2SourceVoice::GetState,用來判斷來源 voice 正在播放的緩衝區數目。 它會呼叫 IXAudio2SourceVoice::SubmitSourceBuffer,以便在沒有緩衝區作用時,將音效的音訊資料新增至聲音輸入佇列。 Audio::PlaySoundEffect 方法也可以讓衝突音效順序播放兩次。 例如,當彈珠與迷宮的角落相撞時,就會發生這種情況。
如先前所述,Audio 類別在初始化滾動事件的聲音時,會使用 XAUDIO2_LOOP_INFINITE 標誌。 聲音會在第一次針對此事件呼叫 Audio::PlaySoundEffect 時,開始迴圈播放。 為了簡化滾動音效的播放邏輯,Marble Maze 會讓聲音靜音,而不是停止音效。 隨著彈珠的速度變化,Marble Maze 會改變聲音的音調和音量,使其更具現實效果。 以下說明 MarbleMazeMain::Update 方法如何隨著彈珠速度的變化來更新音調和音量,以及當彈珠停止時,通過將音量設為零來使聲音靜音的過程。
// Play the roll sound only if the marble is actually rolling.
if (ci.isRollingOnFloor && volume > 0)
{
if (!m_audio.IsSoundEffectStarted(RollingEvent))
{
m_audio.PlaySoundEffect(RollingEvent);
}
// Update the volume and pitch by the velocity.
m_audio.SetSoundEffectVolume(RollingEvent, volume);
m_audio.SetSoundEffectPitch(RollingEvent, pitch);
// The rolling sound has at most 8000Hz sounds, so we linearly
// ramp up the low-pass filter the faster we go.
// We also reduce the Q-value of the filter, starting with a
// relatively broad cutoff and get progressively tighter.
m_audio.SetSoundEffectFilter(
RollingEvent,
600.0f + 8000.0f * volume,
XAUDIO2_MAX_FILTER_ONEOVERQ - volume*volume
);
}
else
{
m_audio.SetSoundEffectVolume(RollingEvent, 0);
}
回應暫停和繼續事件
Marble Maze 應用程式結構 說明了 Marble Maze 如何支援暫停與恢復。 當遊戲暫停時,遊戲會暫停音訊。 當遊戲恢復時,音訊會從中斷處繼續播放。 我們這樣做是為了遵循當您知道不需要資源時不使用資源的最佳實踐。
當遊戲暫停時,會呼叫 Audio::SuspendAudio 方法。 這個方法會呼叫 IXAudio2::StopEngine 方法來停止所有音訊。 雖然 IXAudio2::StopEngine 會立即停止所有音訊輸出,但它會保留音訊圖形及其效果參數(例如彈珠彈跳時套用的殘響效果)。
// Uses the IXAudio2::StopEngine method to stop all audio immediately.
// It leaves the audio graph untouched, which preserves all effect parameters
// and effect histories (like reverb effects) voice states, pending buffers,
// cursor positions and so on.
// When the engines are restarted, the resulting audio will sound as if it had
// never been stopped except for the period of silence.
void Audio::SuspendAudio()
{
if (m_engineExperiencedCriticalError)
{
return;
}
if (m_isAudioStarted)
{
m_musicEngine->StopEngine();
m_soundEffectEngine->StopEngine();
}
m_isAudioStarted = false;
}
當遊戲恢復時,會呼叫 Audio::ResumeAudio 方法。 這個方法會使用 IXAudio2::StartEngine 方法來重新啟動音訊。 因為呼叫 IXAudio2::StopEngine 會保留音頻圖形及其效果參數,所以音頻輸出會從中斷處恢復。
// Restarts the audio streams. A call to this method must match a previous call
// to SuspendAudio. This method causes audio to continue where it left off.
// If there is a problem with the restart, the m_engineExperiencedCriticalError
// flag is set. The next call to Render will recreate all the resources and
// reset the audio pipeline.
void Audio::ResumeAudio()
{
if (m_engineExperiencedCriticalError)
{
return;
}
HRESULT hr = m_musicEngine->StartEngine();
HRESULT hr2 = m_soundEffectEngine->StartEngine();
if (FAILED(hr) || FAILED(hr2))
{
m_engineExperiencedCriticalError = true;
}
}
處理耳機和裝置變更
Marble Maze 會使用引擎回呼函數來處理 XAudio2 引擎的故障,例如在音訊裝置變更時。 裝置變更的可能原因是遊戲用戶連接或斷開耳機。 會建議您實作處理裝置變更的引擎回呼函式。 否則,當使用者插入或移除耳機時,您的遊戲將會停止播放音效,直到遊戲重新啟動為止。
Audio.h 定義 AudioEngineCallbacks 類別。 這個類別會實作 IXAudio2EngineCallback 介面。
class AudioEngineCallbacks: public IXAudio2EngineCallback
{
private:
Audio* m_audio;
public :
AudioEngineCallbacks(){};
void Initialize(Audio* audio);
// Called by XAudio2 just before an audio processing pass begins.
void _stdcall OnProcessingPassStart(){};
// Called just after an audio processing pass ends.
void _stdcall OnProcessingPassEnd(){};
// Called when a critical system error causes XAudio2
// to be closed and restarted. The error code is given in Error.
void _stdcall OnCriticalError(HRESULT Error);
};
IXAudio2EngineCallback 介面可讓您的程式代碼在音訊處理事件發生時以及引擎遇到嚴重錯誤時收到通知。 若要註冊回呼,Marble Maze 會在建立音樂引擎的 IXAudio2 物件後,於 Audio::CreateResources中呼叫 IXAudio2::RegisterForCallbacks 方法。
m_musicEngineCallback.Initialize(this);
m_musicEngine->RegisterForCallbacks(&m_musicEngineCallback);
Marble Maze 不需要在音訊處理開始或結束時進行通知。 因此,它會實作 IXAudio2EngineCallback::OnProcessingPassStart,並 IXAudio2EngineCallback::OnProcessingPassEnd 方法、不執行任何動作。 針對 IXAudio2EngineCallback::OnCriticalError 方法,Marble Maze 會呼叫 SetEngineExperiencedCriticalError 方法,該方法會設定 m_engineExperiencedCriticalError 旗標。
// Audio.cpp
// Called when a critical system error causes XAudio2
// to be closed and restarted. The error code is given in Error.
void _stdcall AudioEngineCallbacks::OnCriticalError(HRESULT Error)
{
m_audio->SetEngineExperiencedCriticalError();
}
// Audio.h (Audio class)
// This flag can be used to tell when the audio system
// is experiencing critical errors.
// XAudio2 gives a critical error when the user unplugs
// the headphones and a new speaker configuration is generated.
void SetEngineExperiencedCriticalError()
{
m_engineExperiencedCriticalError = true;
}
發生嚴重錯誤時,音訊處理會停止,而對 XAudio2 的所有其他呼叫都會失敗。 若要從這種情況中復原,您必須釋放 XAudio2 實例並建立新的實例。 Audio::Render 方法在遊戲迴圈的每一幀(frame)被呼叫,首先檢查 m_engineExperiencedCriticalError 旗標。 如果設定此旗標,它會清除旗標、釋放目前的 XAudio2 實例、初始化資源,然後啟動背景音樂。
if (m_engineExperiencedCriticalError)
{
m_engineExperiencedCriticalError = false;
ReleaseResources();
Initialize();
CreateResources();
Start();
if (m_engineExperiencedCriticalError)
{
return;
}
}
Marble Maze 也會使用 m_engineExperiencedCriticalError 旗標,以防止在沒有音訊裝置可用時呼叫 XAudio2。 例如,MarbleMazeMain::Update 函式不會在當這個旗標被設定時處理滾動或衝突事件的音訊。 如果需要,應用程式會在每個畫面嘗試修復音訊引擎;然而,如果電腦沒有音訊裝置,或耳機未插上且無其他可用的音訊裝置,則 m_engineExperiencedCriticalError 旗標可能會一直設置。
謹慎
依規則,請勿在引擎回呼主體中執行封鎖作業。 這樣做可能會導致效能問題。 Marble Maze 會在 OnCriticalError 回調中設定旗標,稍後會在常規音訊處理階段處理錯誤。 如需 XAudio2 回呼的詳細資訊,請參閱 XAudio2 回呼。
結論
這就是 Marble Maze 遊戲範例的結束! 儘管這款遊戲相對簡單,但它包含了許多任何 UWP DirectX 遊戲中重要的部分,是製作自己遊戲時參考的好範例。
現在,您已完成跟隨學習,請嘗試動手實驗原始碼,看看會發生什麼事。 或者,請參閱 使用 DirectX 建立簡單的 UWP 遊戲,另一個 UWP DirectX 遊戲範例。
準備好進一步使用 DirectX 了嗎? 然後查看我們的 DirectX 程式設計指南 ,。
如果您對 UWP 上的遊戲開發感興趣,請參閱 遊戲程式設計中的檔。