添加声音

注释

本主题是 使用 DirectX 教程系列创建简单的通用 Windows 平台(UWP)游戏的一部分。 该链接中的主题设置序列的上下文。

在本主题中,我们将使用 XAudio2 API 创建简单的声音引擎。 如果你不熟悉 XAudio2,我们在 音频概念下包含了一个简短简介。

注释

如果尚未下载此示例的最新游戏代码,请转到 Direct3D 示例游戏。 此示例是大量 UWP 功能示例集合的一部分。 有关如何下载示例的说明,请参阅 适用于 Windows 开发的示例应用程序。

目的

使用 XAudio2将声音添加到示例游戏中。

定义音频引擎

在示例游戏中,音频对象和行为在三个文件中定义:

  • Audio.h/.cpp:定义 音频 对象,该对象包含用于播放声音的 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接口将 .wav 音频文件读取为脉冲代码调制(PCM)缓冲区。

设置源读取器

  1. 使用 MFCreateSourceReaderFromURL 创建媒体源读取器(IMFSourceReader)。
  2. 使用 MFCreateMediaType 创建媒体类型(IMFMediaType) 对象(mediaType)。 这是对媒体格式的描述。
  3. 指定 mediaType的解码输出是 PCM 音频,这是 XAudio2 可以使用的音频类型。
  4. 通过调用 IMFSourceReader::SetCurrentMediaType为源读取器设置解码的输出媒体类型。

有关为何使用源阅读器的详细信息,请转到 源阅读器

描述音频流的数据格式

  1. 使用 IMFSourceReader::GetCurrentMediaType 获取流的当前媒体类型。
  2. 使用 IMFMediaType::MFCreateWaveFormatExFromMFMediaType 将当前音频媒体类型转换为 WAVEATEX 缓冲区,使用早期操作的结果作为输入。 此结构指定加载音频后使用的波形音频流的数据格式。

WAVEFORMATEX 格式可用于描述 PCM 缓冲区。 与 WAVEFORMATEXTENSIBLE 结构相比,该结构只能用于描述音频波格式的一部分子集。 有关 波形格式波形格式扩展之间的差异的详细信息,请参阅 可扩展 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 播放声音。 它使用 音频 类提供的主语音。 可以使用 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:
    ...
};