加入聲音
注意
本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。
在本主題中,我們會使用 XAudio2 API 建立簡單的音效引擎。 如果您不熟悉 XAudio2,歡迎參閱我們在<Audio concepts>中提供的簡介資訊。
注意
如果您尚未下載此範例的最新遊戲程式碼,請至 Direct3D 範例遊戲頁面下載。 此範例屬於大型 UWP 功能範例集。 如需範例下載的相關指示,請參閱<適用 Windows 開發的範例應用程式>。
目標
使用 XAudio2 將音效新增至範例遊戲。
定義音訊引擎
在範例遊戲中,我們會透過以下三個檔案定義音訊物件和行為:
- Audio.h/.cpp:定義 Audio 物件,其中包含音訊播放的 XAudio2 資源。 此檔案也會定義遊戲暫停或停用時,暫停及繼續播放音訊的方法。
- MediaReader.h/.cpp:定義從本機記憶體讀取音訊 .wav 檔案的方法。
- SoundEffect.h/.cpp:定義遊戲內音效播放的物件。
概觀
在遊戲中設定音訊播放有三個主要流程。
這些全都在 Simple3DGame::Initialize 方法中定義。 因此,我們先檢視此方法再深入說明各部分內容。
完成設定之後,我們將瞭解如何觸發音效播放。 如需詳細資訊,請移至<播放音效>頁面。
Simple3DGame::Initialize 方法
在 Simple3DGame::Initialize (其中 m_controller 和 m_renderer 也已初始化) 中,設定音訊引擎並準備好播放音效。
- 建立 m_audioController,其為 Audio 類別的執行個體。
- 使用 Audio::CreateDeviceIndependentResources 方法建立需要的音訊資源。 在此步驟,我們會建立兩個 XAudio2 物件 (音樂引擎物件和音效引擎物件),並對其進行後製聲音處理。 音樂引擎物件可用來播放遊戲的背景音樂。 音效引擎可用來在遊戲中播放音效。 如需詳細資訊,請參閱<建立和初始化音訊資源>。
- 建立 mediaReader,其為 MediaReader 類別的執行個體。 MediaReader 是 SoundEffect 類別的協助程式類別,可從檔案位置同步讀取小型音訊檔案,並以位元組陣列傳回音效資料。
- 使用 MediaReader::LoadMedia 從其位置載入音效檔案,並建立 targetHitSound 變數以保存載入的 .wav 音效資料。 如需詳細資訊,請參閱<載入音訊檔案>。
音效與遊戲物件相關聯。 因此,若與該遊戲物件發生衝突,便會觸發要播放的音效。 在此範例遊戲中,我們有彈藥 (用於射擊目標) 及目標專用的音效。
- GameObject 類別中的 HitSound 屬性可用來將音效與物件產生關聯。
- 建立 SoundEffect 類別的新執行個體,並將其初始化。 初始化期間會建立音效的來源聲音。
- 此類別會使用 Audio 類別提供的後製聲音播放音效。 我們使用 MediaReader 類別從檔案位置讀取音訊資料。 如需詳細資訊,請參閱<將音效與物件產生關聯>。
注意
播放音效的實際觸發程序取決於這些遊戲物件的動作和衝突。 因此,實際播放這些音效的呼叫會在 Simple3DGame::UpdateDynamics 方法中定義。 如需詳細資訊,請移至<播放音效>頁面。
void Simple3DGame::Initialize(
_In_ std::shared_ptr<MoveLookController> const& controller,
_In_ std::shared_ptr<GameRenderer> const& renderer
)
{
// The following member is defined in the header file:
// Audio m_audioController;
...
// Create the audio resources needed.
// Two XAudio2 objects are created - one for music engine,
// the other for sound engine. A mastering voice is also
// created for each of the objects.
m_audioController.CreateDeviceIndependentResources();
m_ammo.resize(GameConstants::MaxAmmo);
...
// Create a media reader which is used to read audio files from its file location.
MediaReader mediaReader;
auto targetHitSoundX = mediaReader.LoadMedia(L"Assets\\hit.wav");
// Instantiate the targets for use in the game.
// Each target has a different initial position, size, and orientation.
// But share a common set of material properties.
for (int a = 1; a < GameConstants::MaxTargets; a++)
{
...
// Create a new sound effect object and associate it
// with the game object's (target) HitSound property.
target->HitSound(std::make_shared<SoundEffect>());
// Initialize the sound effect object with
// the sound effect engine, format of the audio wave, and audio data
// During initialization, source voice of this sound effect is also created.
target->HitSound()->Initialize(
m_audioController.SoundEffectEngine(),
mediaReader.GetOutputWaveFormatEx(),
targetHitSoundX
);
...
}
// Instantiate a set of spheres to be used as ammunition for the game
// and set the material properties of the spheres.
auto ammoHitSound = mediaReader.LoadMedia(L"Assets\\bounce.wav");
for (int a = 0; a < GameConstants::MaxAmmo; a++)
{
m_ammo[a] = std::make_shared<Sphere>();
m_ammo[a]->Radius(GameConstants::AmmoRadius);
m_ammo[a]->HitSound(std::make_shared<SoundEffect>());
m_ammo[a]->HitSound()->Initialize(
m_audioController.SoundEffectEngine(),
mediaReader.GetOutputWaveFormatEx(),
ammoHitSound
);
m_ammo[a]->Active(false);
m_renderObjects.push_back(m_ammo[a]);
}
...
}
建立和初始化音訊資源
- 使用 XAudio2Create (XAudio2 API) 建立兩個新的 XAudio2 物件,其用於定義音樂和音效引擎。 此方法會傳回物件的 IXAudio2 介面指標,該介面管理所有音訊引擎狀態、音訊處理執行緒、聲音圖形等。
- 引擎具現化之後,請使用 IXAudio2::CreateMasteringVoice 為每個音效引擎物件建立後製聲音。
如需詳細資訊,請參閱<操作說明:初始化 XAudio2>。
Audio::CreateDeviceIndependentResources 方法
void Audio::CreateDeviceIndependentResources()
{
UINT32 flags = 0;
winrt::check_hresult(
XAudio2Create(m_musicEngine.put(), flags)
);
HRESULT hr = m_musicEngine->CreateMasteringVoice(&m_musicMasteringVoice);
if (FAILED(hr))
{
// Unable to create an audio device
m_audioAvailable = false;
return;
}
winrt::check_hresult(
XAudio2Create(m_soundEffectEngine.put(), flags)
);
winrt::check_hresult(
m_soundEffectEngine->CreateMasteringVoice(&m_soundEffectMasteringVoice)
);
m_audioAvailable = true;
}
載入音訊檔案
在範例遊戲中,讀取音訊格式檔案的程式碼是在 MediaReader.h/cpp__ 中定義。 若要讀取已編碼的 .wav 音訊檔案,請呼叫 MediaReader::LoadMedia,以輸入參數的形式傳入 .wav 的檔案名稱。
MediaReader::LoadMedia 方法
此方法使用 Media Foundation API,以 Pulse Code Modulation (PCM) 緩衝區形式在 .wav 音訊檔案中讀取。
設定來源讀取器
- 使用 MFCreateSourceReaderFromURL 建立媒體來源讀取器 (IMFSourceReader)。
- 使用 MFCreateMediaType 建立媒體類型 (IMFMediaType) 物件 (mediaType)。 其代表媒體格式的描述。
- 指定 mediaType 的解碼輸出為 PCM 音訊,其為 XAudio2 可使用的音訊類型。
- 呼叫 IMFSourceReader::SetCurrentMediaType,為來源讀取器設定解碼輸出的媒體類型。
如需為何使用來源讀取器的詳細資訊,請前往<來源讀取器>頁面。
描述音訊串流的資料格式
- 使用 IMFSourceReader::GetCurrentMediaType 取得串流目前的媒體類型。
- 使用 IMFMediaType::MFCreateWaveFormatExFromMFMediaType 將目前的音訊媒體類型轉換為 WAVEFORMATEX 緩衝區,並以先前的作業結果當作輸入。 此結構指定音訊載入後所用的聲波音訊串流資料格式。
WAVEFORMATEX 格式可用來描述 PCM 緩衝區。 相較於 WAVEFORMATEXTENSIBLE 結構,其只能用於描述音訊聲波格式的子集。 如需有關 WAVEFORMATEX 和 WAVEFORMATEXTENSIBLE 差異的詳細資訊,請參閱<可延伸聲波格式描述元>。
讀取音訊串流
- 呼叫 IMFSourceReader::GetPresentationAttribute 以取得音訊串流的持續期間 (以秒為單位),然後將該期間轉換為位元組。
- 呼叫 IMFSourceReader::ReadSample,以串流形式讀取音訊檔案。 ReadSample 會從媒體來源讀取下一個範例。
- 使用 IMFSample::ConvertToContiguousBuffer 將音訊範例緩衝區 (sample) 的內容複製到陣列 (mediaBuffer)。
std::vector<byte> MediaReader::LoadMedia(_In_ winrt::hstring const& filename)
{
winrt::check_hresult(
MFStartup(MF_VERSION)
);
// Creates a media source reader.
winrt::com_ptr<IMFSourceReader> reader;
winrt::check_hresult(
MFCreateSourceReaderFromURL(
(m_installedLocationPath + filename).c_str(),
nullptr,
reader.put()
)
);
// Set the decoded output format as PCM.
// XAudio2 on Windows can process PCM and ADPCM-encoded buffers.
// When using MediaFoundation, this sample always decodes into PCM.
winrt::com_ptr<IMFMediaType> mediaType;
winrt::check_hresult(
MFCreateMediaType(mediaType.put())
);
// Define the major category of the media as audio. For more info about major media types,
// go to: https://msdn.microsoft.com/library/windows/desktop/aa367377.aspx
winrt::check_hresult(
mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)
);
// Define the sub-type of the media as uncompressed PCM audio. For more info about audio sub-types,
// go to: https://msdn.microsoft.com/library/windows/desktop/aa372553.aspx
winrt::check_hresult(
mediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)
);
// Sets the media type for a stream. This media type defines that format that the Source Reader
// produces as output. It can differ from the native format provided by the media source.
// For more info, go to https://msdn.microsoft.com/library/windows/desktop/dd374667.aspx
winrt::check_hresult(
reader->SetCurrentMediaType(static_cast<uint32_t>(MF_SOURCE_READER_FIRST_AUDIO_STREAM), 0, mediaType.get())
);
// Get the current media type for the stream.
// For more info, go to:
// https://msdn.microsoft.com/library/windows/desktop/dd374660.aspx
winrt::com_ptr<IMFMediaType> outputMediaType;
winrt::check_hresult(
reader->GetCurrentMediaType(static_cast<uint32_t>(MF_SOURCE_READER_FIRST_AUDIO_STREAM), outputMediaType.put())
);
// Converts the current media type into the WaveFormatEx buffer structure.
UINT32 size = 0;
WAVEFORMATEX* waveFormat;
winrt::check_hresult(
MFCreateWaveFormatExFromMFMediaType(outputMediaType.get(), &waveFormat, &size)
);
// Copies the waveFormat's block of memory to the starting address of the m_waveFormat variable in MediaReader.
// Then free the waveFormat memory block.
// For more info, go to https://msdn.microsoft.com/library/windows/desktop/aa366535.aspx and
// https://msdn.microsoft.com/library/windows/desktop/ms680722.aspx
CopyMemory(&m_waveFormat, waveFormat, sizeof(m_waveFormat));
CoTaskMemFree(waveFormat);
PROPVARIANT propVariant;
winrt::check_hresult(
reader->GetPresentationAttribute(static_cast<uint32_t>(MF_SOURCE_READER_MEDIASOURCE), MF_PD_DURATION, &propVariant)
);
// 'duration' is in 100ns units; convert to seconds, and round up
// to the nearest whole byte.
LONGLONG duration = propVariant.uhVal.QuadPart;
unsigned int maxStreamLengthInBytes =
static_cast<unsigned int>(
((duration * static_cast<ULONGLONG>(m_waveFormat.nAvgBytesPerSec)) + 10000000) /
10000000
);
std::vector<byte> fileData(maxStreamLengthInBytes);
winrt::com_ptr<IMFSample> sample;
winrt::com_ptr<IMFMediaBuffer> mediaBuffer;
DWORD flags = 0;
int positionInData = 0;
bool done = false;
while (!done)
{
// Read audio data.
...
}
return fileData;
}
將音效與物件產生關聯
遊戲初始化時,系統會在 Simple3DGame::Initialize 方法中建立音訊與物件的關聯。
概括回顧:
- GameObject 類別中的 HitSound 屬性可用來將音效與物件產生關聯。
- 建立 SoundEffect 類別物件的新執行個體,並將其與遊戲物件產生關聯。 此類別使用 XAudio2 API 播放音效。 其使用 Audio 類別提供的後製聲音。 我們可使用 MediaReader 類別從檔案位置讀取音效資料。
SoundEffect::Initialize 用於初始化 SoundEffect 執行個體,其中採用下列輸入參數:音效引擎物件指標 (在 Audio::CreateDeviceIndependentResources 方法中建立的 IXAudio2 物件)、.wav 檔案格式指標 (使用 MediaReader::GetOutputWaveFormatEx),以及使用 MediaReader::LoadMedia 方法載入的音效資料。 初始化期間,也會建立音效的來源聲音。
SoundEffect::Initialize 方法
void SoundEffect::Initialize(
_In_ IXAudio2* masteringEngine,
_In_ WAVEFORMATEX* sourceFormat,
_In_ std::vector<byte> const& soundData)
{
m_soundData = soundData;
if (masteringEngine == nullptr)
{
// Audio is not available so just return.
m_audioAvailable = false;
return;
}
// Create a source voice for this sound effect.
winrt::check_hresult(
masteringEngine->CreateSourceVoice(
&m_sourceVoice,
sourceFormat
)
);
m_audioAvailable = true;
}
播放音效
播放音效的觸發程序是在 Simple3DGame::UpdateDynamics 方法中定義,因為系統是透過該方法更新物件移動的位置,以及判定物件之間發生衝突。
視遊戲而定,物件之間的互動差異可能很大,因此這裡不討論遊戲物件的動態情況。 若想瞭解相關實作,請參閱 Simple3DGame::UpdateDynamics 方法。
原則上,發生衝突時,系統會呼叫 SoundEffect::PlaySound 觸發音效播放。 此方法會停止目前正在播放的任何音效,並將指定音效資料排入記憶體內部緩衝區的佇列。 該方法會以來源聲音設定音量、提交音效資料並開始播放。
SoundEffect::PlaySound 方法
- 使用來源聲音物件 m_sourceVoice 開始播放音效資料緩衝區 m_soundData
- 建立 XAUDIO2_BUFFER,並向其提供音訊資料緩衝區參照,然後使用 IXAudio2SourceVoice::SubmitSourceBuffer 呼叫進行提交。
- 音效資料排入佇列後,SoundEffect::PlaySound 會呼叫 IXAudio2SourceVoice::Start 以開始播放。
void SoundEffect::PlaySound(_In_ float volume)
{
XAUDIO2_BUFFER buffer = { 0 };
if (!m_audioAvailable)
{
// Audio is not available so just return.
return;
}
// Interrupt sound effect if it is currently playing.
winrt::check_hresult(
m_sourceVoice->Stop()
);
winrt::check_hresult(
m_sourceVoice->FlushSourceBuffers()
);
// Queue the memory buffer for playback and start the voice.
buffer.AudioBytes = (UINT32)m_soundData.size();
buffer.pAudioData = m_soundData.data();
buffer.Flags = XAUDIO2_END_OF_STREAM;
winrt::check_hresult(
m_sourceVoice->SetVolume(volume)
);
winrt::check_hresult(
m_sourceVoice->SubmitSourceBuffer(&buffer)
);
winrt::check_hresult(
m_sourceVoice->Start()
);
}
Simple3DGame::UpdateDynamics 方法
Simple3DGame::UpdateDynamics 方法會處理遊戲物件之間的互動和衝突。 物件衝突 (或交集) 時會觸發相關聯的音效播放。
void Simple3DGame::UpdateDynamics()
{
...
// Check for collisions between ammo.
#pragma region inter-ammo collision detection
if (m_ammoCount > 1)
{
...
// Check collision between instances One and Two.
...
if (distanceSquared < (GameConstants::AmmoSize * GameConstants::AmmoSize))
{
// The two ammo are intersecting.
...
// Start playing the sounds for the impact between the two balls.
m_ammo[one]->PlaySound(impact, m_player->Position());
m_ammo[two]->PlaySound(impact, m_player->Position());
}
}
#pragma endregion
#pragma region Ammo-Object intersections
// Check for intersections between the ammo and the other objects in the scene.
// ...
// Ball is in contact with Object.
// ...
// Make sure that the ball is actually headed towards the object. At grazing angles there
// could appear to be an impact when the ball is actually already hit and moving away.
if (impact > 0.0f)
{
...
// Play the sound associated with the Ammo hitting something.
m_objects[i]->PlaySound(impact, m_player->Position());
if (m_objects[i]->Target() && !m_objects[i]->Hit())
{
// The object is a target and isn't currently hit, so mark
// it as hit and play the sound associated with the impact.
m_objects[i]->Hit(true);
m_objects[i]->HitTime(timeTotal);
m_totalHits++;
m_objects[i]->PlaySound(impact, m_player->Position());
}
...
}
#pragma endregion
#pragma region Apply Gravity and world intersection
// Apply gravity and check for collision against enclosing volume.
...
if (position.z < limit)
{
// The ammo instance hit the a wall in the min Z direction.
// Align the ammo instance to the wall, invert the Z component of the velocity and
// play the impact sound.
position.z = limit;
m_ammo[i]->PlaySound(-velocity.z, m_player->Position());
velocity.z = -velocity.z * GameConstants::Physics::GroundRestitution;
}
...
#pragma endregion
}
下一步
我們已經介紹了適用 Windows 10 遊戲的 UWP 架構、圖形、控制項、使用者介面和音訊。 本教學課程的下個部分是<延伸範例遊戲>,說明開發遊戲時可使用的其他選項。
音訊概念
針對 Windows 10 遊戲開發作業,請使用 XAudio2 2.9 版。 此版本隨附於 Windows 10。 如需詳細資訊,請前往<XAudio2 版本>頁面。
AudioX2 屬於低階 API,提供訊號處理和混合基礎。 如需詳細資訊,請參閱<XAudio2 重要概念>。
XAudio2 聲音
XAudio2 聲音物件有三種類型:來源、子混音和後製聲音。 Voices 是 XAudio2 用來處理、操作及播放音訊資料的物件。
- 來源聲音對用戶端提供的音訊資料進行處理。
- 來源聲音和子混音將其輸出傳送至一或多個子混音或後製聲音。
- 子混音和後製聲音混合所有輸入的聲音,並對結果進行處理。
- 後製聲音接收來自來源聲音和子混音的資料,並將該資料傳送至音訊硬體。
如需詳細資訊,請前往<XAudio2 聲音>頁面。
音訊圖
音訊圖是 XAudio2 聲音的集合。 音訊從來源聲音中的音訊圖一端開始,接著選擇性傳送一或多個子混音,然後結束於後製聲音。 音訊圖包含目前播放中每個音效的來源聲音、零或多個子混音,以及一個後製聲音。 直接輸出至後製聲音的單一來源聲音,是最簡單的音訊圖,也是在 XAudio2 中產生噪音的最低門檻。 如需詳細資訊,請前往<音訊圖>頁面。
延伸閱讀
重要 audio.h 檔案
Audio.h
// Audio:
// This class uses XAudio2 to provide sound output. It creates two
// engines - one for music and the other for sound effects - each as
// a separate mastering voice.
// The SuspendAudio and ResumeAudio methods can be used to stop
// and start all audio playback.
class Audio
{
public:
Audio();
void Initialize();
void CreateDeviceIndependentResources();
IXAudio2* MusicEngine();
IXAudio2* SoundEffectEngine();
void SuspendAudio();
void ResumeAudio();
private:
...
};
MediaReader.h
// MediaReader:
// This is a helper class for the SoundEffect class. It reads small audio files
// synchronously from the package installed folder and returns sound data as a
// vector of bytes.
class MediaReader
{
public:
MediaReader();
std::vector<byte> LoadMedia(_In_ winrt::hstring const& filename);
WAVEFORMATEX* GetOutputWaveFormatEx();
private:
winrt::Windows::Storage::StorageFolder m_installedLocation{ nullptr };
winrt::hstring m_installedLocationPath;
WAVEFORMATEX m_waveFormat;
};
SoundEffect.h
// SoundEffect:
// This class plays a sound using XAudio2. It uses a mastering voice provided
// from the Audio class. The sound data can be read from disk using the MediaReader
// class.
class SoundEffect
{
public:
SoundEffect();
void Initialize(
_In_ IXAudio2* masteringEngine,
_In_ WAVEFORMATEX* sourceFormat,
_In_ std::vector<byte> const& soundData
);
void PlaySound(_In_ float volume);
private:
...
};
意見反應
https://aka.ms/ContentUserFeedback。
即將登場:在 2024 年,我們將逐步淘汰 GitHub 問題作為內容的意見反應機制,並將它取代為新的意見反應系統。 如需詳細資訊,請參閱:提交並檢視相關的意見反應