加入聲音

注意

本主題屬於<使用 DirectX 建立簡單的通用 Windows 平台 (UWP) 遊戲>教學課程系列的一部分。 該連結主題是提供這系列教學的基本背景介紹。

在本主題中,我們會使用 XAudio2 API 建立簡單的音效引擎。 如果您不熟悉 XAudio2,歡迎參閱我們在<Audio concepts>中提供的簡介資訊。

注意

如果您尚未下載此範例的最新遊戲程式碼,請至 Direct3D 範例遊戲頁面下載。 此範例屬於大型 UWP 功能範例集。 如需範例下載的相關指示,請參閱<適用 Windows 開發的範例應用程式>。

目標

使用 XAudio2 將音效新增至範例遊戲。

定義音訊引擎

在範例遊戲中,我們會透過以下三個檔案定義音訊物件和行為:

  • Audio.h/.cpp:定義 Audio 物件,其中包含音訊播放的 XAudio2 資源。 此檔案也會定義遊戲暫停或停用時,暫停及繼續播放音訊的方法。
  • MediaReader.h/.cpp:定義從本機記憶體讀取音訊 .wav 檔案的方法。
  • SoundEffect.h/.cpp:定義遊戲內音效播放的物件。

概觀

在遊戲中設定音訊播放有三個主要流程。

  1. 建立和初始化音訊資源
  2. 載入音訊檔案
  3. 將音效與物件產生關聯

這些全都在 Simple3DGame::Initialize 方法中定義。 因此,我們先檢視此方法再深入說明各部分內容。

完成設定之後,我們將瞭解如何觸發音效播放。 如需詳細資訊,請移至<播放音效>頁面。

Simple3DGame::Initialize 方法

Simple3DGame::Initialize (其中 m_controllerm_renderer 也已初始化) 中,設定音訊引擎並準備好播放音效。

  • 建立 m_audioController,其為 Audio 類別的執行個體。
  • 使用 Audio::CreateDeviceIndependentResources 方法建立需要的音訊資源。 在此步驟,我們會建立兩個 XAudio2 物件 (音樂引擎物件和音效引擎物件),並對其進行後製聲音處理。 音樂引擎物件可用來播放遊戲的背景音樂。 音效引擎可用來在遊戲中播放音效。 如需詳細資訊,請參閱<建立和初始化音訊資源>。
  • 建立 mediaReader,其為 MediaReader 類別的執行個體。 MediaReaderSoundEffect 類別的協助程式類別,可從檔案位置同步讀取小型音訊檔案,並以位元組陣列傳回音效資料。
  • 使用 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 音訊檔案中讀取。

設定來源讀取器

  1. 使用 MFCreateSourceReaderFromURL 建立媒體來源讀取器 (IMFSourceReader)。
  2. 使用 MFCreateMediaType 建立媒體類型 (IMFMediaType) 物件 (mediaType)。 其代表媒體格式的描述。
  3. 指定 mediaType 的解碼輸出為 PCM 音訊,其為 XAudio2 可使用的音訊類型。
  4. 呼叫 IMFSourceReader::SetCurrentMediaType,為來源讀取器設定解碼輸出的媒體類型。

如需為何使用來源讀取器的詳細資訊,請前往<來源讀取器>頁面。

描述音訊串流的資料格式

  1. 使用 IMFSourceReader::GetCurrentMediaType 取得串流目前的媒體類型。
  2. 使用 IMFMediaType::MFCreateWaveFormatExFromMFMediaType 將目前的音訊媒體類型轉換為 WAVEFORMATEX 緩衝區,並以先前的作業結果當作輸入。 此結構指定音訊載入後所用的聲波音訊串流資料格式。

WAVEFORMATEX 格式可用來描述 PCM 緩衝區。 相較於 WAVEFORMATEXTENSIBLE 結構,其只能用於描述音訊聲波格式的子集。 如需有關 WAVEFORMATEXWAVEFORMATEXTENSIBLE 差異的詳細資訊,請參閱<可延伸聲波格式描述元>。

讀取音訊串流

  1. 呼叫 IMFSourceReader::GetPresentationAttribute 以取得音訊串流的持續期間 (以秒為單位),然後將該期間轉換為位元組。
  2. 呼叫 IMFSourceReader::ReadSample,以串流形式讀取音訊檔案。 ReadSample 會從媒體來源讀取下一個範例。
  3. 使用 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 方法

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:
    ...
};