Share via


在 Marble Maze 範例中加入音訊

本文件介紹了使用音訊時要考慮的關鍵實踐,並展示了 Marble Maze 如何應用這些實踐。 Marble Maze 使用媒體基礎從檔案載入音訊資源,並使用 XAudio2 來混合和播放音訊以及對音訊應用效果。

Marble Maze 在背景播放音樂,也使用遊戲音效來表示遊戲事件,例如彈珠撞牆時。 實作的一個重要部分是 Marble Maze 會在彈跳時使用回聲或回聲效果來模擬彈珠的聲音。 殘響效果的實現使迴響在小房間裡更快、更響亮地傳到您耳中;在較大的房間中,迴聲更安靜,到達您的速度也更慢。

注意

與本文檔對應的範例程式碼可以在 DirectX Marble Maze 遊戲範例中找到。

以下是本文件說明在遊戲中使用音訊時的一些重點:

  • 考慮使用媒體基礎解碼音訊資源並使用 XAudio2 播放音訊。 但是,如果您有可在通用 Windows 平台 (UWP) 應用程式中運行的現有音訊資源載入機制,則可以使用它。

  • 音訊圖包含每個活動聲音的一種來源聲音、零個或多個子混合聲音以及一種母帶聲音。 來源聲音可以輸入到子混合聲音和/或母帶聲音中。 子混合聲音輸入到其他子混合聲音或母帶聲音。

  • 如果您的背景音樂檔案很大,請考慮將音樂串流到較小的緩衝區中,以便使用更少的記憶體。

  • 如果這樣做有意義,請在應用程式失去焦點或可見性或暫停時暫停音訊播放。 當您的應用程式重新獲得焦點、變得可見或恢復時恢復播放。

  • 設定音訊類別以反映每種聲音的作用。 例如,您通常會針對遊戲背景音訊使用 AudioCategory_GameMedia,並針對音效使用 AudioCategory_GameEffects

  • 透過釋放和重新建立所有音訊資源和介面來處理設備變更 (包括耳機)。

  • 當需要最小化磁碟空間和串流媒體成本時,請考慮是否壓縮音訊檔案。 否則,您可以保留音訊未壓縮,以便載入速度更快。

XAudio2 和 Microsoft 媒體基礎簡介

XAudio2 是 Windows 的低階音訊庫,專門支援遊戲音訊。 它為遊戲提供數位訊號處理 (DSP) 和音訊圖形引擎。 XAudio2 透過支援 SIMD 浮點架構和高清音訊等運算趨勢,對其前身 DirectSound 和 XAudio 進行了擴展。 它還支援當今遊戲更複雜的聲音處理需求。

文件 XAudio2 Key Concepts 解釋了使用 XAudio2 的關鍵概念。 簡而言之,這些概念是:

  • IXAudio2 介面是 XAudio2 引擎的核心。 Marble Maze 會使用此介面來建立語音,並在輸出裝置變更或失敗時接收通知。

  • 語音會處理、調整及播放音訊資料。

  • 來源語音是音訊通道 (單聲道、5.1 等) 的集合,代表一個音訊資料流。 在 XAudio2 中,來源語音是音訊處理開始的地方。 一般而言,聲音資料會從外部來源載入,例如檔案或網路,並傳送至來源語音。 Marble Maze 使用媒體基礎,從檔案載入聲音資料。 本文件稍後會介紹媒體基礎。

  • 子混音處理音訊資料。 此處理可能包括變更音訊資料流,或將多個資料流合併成一個資料流。 Marble Maze 使用子混音來建立殘響效果。

  • 主控語音結合了來源和子混音語音的資料,並將該資料傳送至音訊硬體。

  • 音訊圖包含每個活動聲音的一種來源聲音、零個或多個子混合聲音以及僅一種母帶聲音。

  • 回調會通知客戶端程式碼語音或引擎物件中發生了某些事件。 透過使用回調,您可以在 XAudio2 完成緩衝區時重複使用內存,在音訊裝置變更時做出反應 (例如,當您連接或斷開耳機時) 等等。 本文件後面的處理耳機和裝置變更解釋了 Marble Maze 如何使用此機制來處理裝置變更。

Marble Maze 使用兩個音訊引擎 (即兩個 IXAudio2 物件) 來處理音訊。 一個引擎會處理背景音樂,另一個引擎會處理遊戲音效。

Marble Maze 也必須為每個引擎建立一個主控語音。 回想一下,母帶製作引擎將音訊串流組合成一個串流並將該串流發送到音訊硬體。 背景音樂串流 (來源語音) 將資料輸出到主控語音和兩個子混合語音。 子混合聲音執行混響效果。

媒體基礎是支援許多音訊和視訊格式的多媒體媒體櫃。 XAudio2 和媒體基礎彼此互補。 Marble Maze 使用媒體基礎從檔案載入音訊資產,並使用 XAudio2 播放音訊。 您不必使用媒體基礎來載入音訊資源。 如果您有適用於通用 Windows 平台 (UWP) 應用程式的現有音訊資源載入機制,請務必使用。 音訊、視訊和相機討論了在 UWP 應用中實現音訊的幾種方法。

如需 XAudio2 的詳細資訊,請參閱屬性 (C# 程式設計手冊)。 如需媒體基礎的詳細資訊,請參閱媒體基礎

初始化音訊資源

Marble Mazes 使用 Windows Media Audio (.wma) 檔案作為背景音樂,使用 WAV (.wav) 檔案作為遊戲聲音。 這些格式由媒體基礎支持。 儘管 XAudio2 本身支援 .wav 檔案格式,但遊戲必須手動解析檔案格式以填寫適當的 XAudio2 資料結構。 Marble Maze 使用媒體基礎,更輕鬆地使用.wav檔案。 有關媒體基礎支援的媒體格式的完整列表,請參閱媒體基礎支援的媒體格式。 Marble Maze 不使用個別的設計時間和運行時間音訊格式,而且不使用 XAudio2 ADPCM 壓縮支援。 如需 XAudio2 中 ADPCM 壓縮的詳細資訊,請參閱 ADPCM 概觀

Audio::CreateResources 方法從 MarbleMazeMain::LoadDeferredResources 調用,從檔案載入音訊串流,初始化 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_musicMasteringVoiceIXAudio2MasteringVoice 物件。 我們指定兩個輸入通道;這簡化了混響效果的邏輯。

我們會將 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 方法執行類似的步驟來建立遊戲聲音的母帶聲音,不同之處在於它為 StreamCategory 參數指定 AudioCategory_GameEffects (預設值)。

建立殘響效果

對於每種聲音,您可以使用 XAudio2 建立處理音訊的效果序列。 這樣的序列稱為效應鏈。 當您想要將一種或多種效果應用於聲音時,請使用效果鏈。 效果鏈可能具有破壞性;也就是說,鏈結中的每個效果都可以覆蓋音訊緩衝區。 此屬性很重要,因為 XAudio2 不保證輸出緩衝區在靜默狀態下初始化。 Effect 物件在 XAudio2 中由跨平台音訊處理物件 (XAPO) 表示。 如需 XAPO 的詳細資訊,請參閱 XAPO 概觀

建立效果鏈時,請依照下列步驟操作:

  1. 建立效果物件。

  2. 使用效果資料填入 XAUDIO2_EFFECT_DESCRIPTOR 結構。

  3. 使用資料填入 XAUDIO2_EFFECT_CHAIN 結構。

  4. 將效果鏈結套用至語音。

  5. 填充效果參數結構並將其應用於效果。

  6. 請視需要停用或啟用效果。

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 kHz 的取樣率。

DX::ThrowIfFailed(
    engine->CreateSubmixVoice(newSubmix, 2, 48000, 0, 0, nullptr, &soundEffectChain)
    );

提示

如果要將現有效果鏈附加到現有子混合語音,或要取代目前效果鏈,請使用 IXAudio2Voice::SetEffectChain 方法。

Audio::CreateReverb 方法呼叫 IXAudio2Voice::SetEffectParameters 來設定與效果相關的其他參數。 這個方法會採用效果特有的參數結構。 包含殘響效果參數的 XAUDIO2FX_REVERB_PARAMETERS 物件 m_reverbParametersSmallAudio::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))
    );

如果設定了 enableEffect 標誌,則 Audio::CreateReverb 方法將透過使用 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_VERSIONmfapi.h 中定義的巨集,是我們指定要使用的 Media Foundation 版本的巨集。

DX::ThrowIfFailed(
    MFStartup(MF_VERSION)
    );

MediaStreamer::Initialize 然後呼叫 MFCreateSourceReaderFromURL 建立 IMFSourceReader 物件。 IMFSourceReader 物件 m_readerurl 指定的檔案中讀取媒體資料。

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_TYPEMF_MT_SUBTYPE媒體基礎屬性MFMediaType_AudioMFAudioFormat_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::GetCurrentMediaTypeMedia Foundation 取得完整的輸出媒體格式,並呼叫 MFCreateWaveFormatExFromMFMediaType 方法將 Media Foundation 音訊媒體類型轉換為 WAVEFORMATEX 結構。 SEMANTICATEX 結構會定義超聲波音頻資料的格式。 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 作為彈珠滾動聲播放。
FallingEvent MarbleFall.wav 彈珠從迷宮中掉下來時播放。
CollisionEvent MarbleHit.wav 彈珠與迷宮相撞時播放。
CheckpointEvent Checkpoint.wav 彈珠經過檢查點時播放。
MenuChangeEvent MenuChange.wav 當用戶變更目前的功能表項時播放。
MenuSelectedEvent 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::SetRoomSizePhysics::CalculateCurrentRoomSize

注意

在大多數房間大小相對相同的遊戲中,您可以使用更基本的混響模型。 例如,您可以為所有房間使用一種混響設置,也可以為每個房間建立預先定義的殘響設置。

Audio::CreateResources 方法使用 Media Foundation 載入背景音樂。 然而,此時來源語音沒有可使用的音訊資料。 另外,由於背景音樂循環播放,因此必須定期更新來源語音資料,以便音樂繼續播放。

為了保持來源語音充滿資料,遊戲循環每個畫面都會更新音訊緩衝區。 MarbleMazeMain::Render 方法呼叫 Audio::Render 來處理背景音樂音訊緩衝區。 Audio 類別定義了一個由三個音訊緩衝區組成的陣列 m_audioBuffers。 每個緩衝區保存 64 KB (65536 位元組) 的資料。 此循環從媒體基礎物件讀取資料並將該資料寫入來源語音,直到來源語音具有三個排隊緩衝區。

警告

雖然 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;
    }
}

該循環還處理媒體基礎物件何時到達串流的末尾。 在這種情況下,它會呼叫 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 類別提供了 PlaySoundEffectIsSoundEffectStartedStopSoundEffectSetSoundEffectVolumeSetSoundEffectPitchSetSoundEffectFilter 等方法,可讓遊戲控制聲音何時播放和停止,以及控制聲音屬性 (例如音量和音調)。 例如,如果彈珠從迷宮中掉落,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 來確定來源語音正在播放的緩衝區數量。 如果沒有活動的緩衝區,它會呼叫 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::OnProcessingPassStartIXAudio2EngineCallback::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 方法每幀從遊戲循環中調用,首先檢查 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 上的遊戲開發感興趣,請參閱遊戲程式設計中的文件。