共用方式為


新增音效

備註

本主題是 使用 DirectX 教學課程系列建立簡單的通用 Windows 平臺 (UWP) 遊戲的一部分。 該連結中的主題設定了整個系列的背景。

在本主題中,我們會使用 XAudio2 API 來建立簡單的音效引擎。 如果您不熟悉 XAudio2,我們已在 音頻概念下提供簡短的介紹。

備註

如果您尚未下載此範例的最新遊戲程式代碼,請移至 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 類別的實例。 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]);
    }
    ...
}

建立和初始化音頻資源

  • 使用 XAudio2CreateXAudio2 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,將 .wav 音訊檔案讀取為 Pulse Code Modulation (PCM)緩衝區。

設定來源讀取器

  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之間差異的更多資訊,請參閱 Extensible Wave-Format 描述符

讀取音訊串流

  1. 呼叫 IMFSourceReader::GetPresentationAttribute ,以秒為單位取得音訊流的持續時間,然後將持續時間轉換為位元組。
  2. 通過呼叫 IMFSourceReader 的 ReadSample 方法,以串流的形式讀取音訊檔案。 ReadSample 會從媒體來源讀取下一個範例。
  3. 使用 IMFSample::ConvertToContiguousBuffer,將音訊範例緩衝區的內容(範例)複製到陣列(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 物件)、使用 MediaReader::GetOutputWaveFormatEx指向 .wav 檔案格式的指標,以及使用 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 語音物件包括三種類型:來源語音、副混音語音和主控語音。 語音是 XAudio2 用來處理、操作及播放音訊數據的物件。

  • 來源語音會在用戶端所提供的音訊數據上運作。
  • 來源語音和副混音語音會將其輸出傳送至一個或多個副混音語音或主控語音。
  • 副混音和混音主控會將所有來源語音的音訊進行混合,然後針對產生的結果進行操作。
  • 主控聲音會接收來自源聲音和子混音的訊號,並將該訊號傳送至音訊硬體。

如需詳細資訊,請前往 XAudio2 語音

音訊圖表

音訊圖是 XAudio2語音的集合。 音訊會從來源語音中的音訊圖形的一端開始,選擇性地通過一或多個副混音,並以主控語音結束。 音頻圖表會包含目前播放的每個音效的來源語音、零或多個副混語音,以及一個主控語音。 最簡單的音頻圖,以及在 XAudio2 中發出聲音所需的最小要求,是單一來源音頻直接輸出到主音頻。 如需詳細資訊,請查看 音訊圖表

進一步閱讀

主要音訊 .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:
    ...
};