添加声音

注意

本主题是使用 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_controller 和 m_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 方法

此方法使用媒体基础 API 作为脉冲编码调制 (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 结构相比,它只能用于描述一小部分音频波形格式。 有关 WAVEFORMATEX 与 WAVEFORMATEXTENSIBLE 之间差异的详细信息,请参阅可扩展的波形格式描述符

读取音频流

  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 方法中,游戏初始化时会将声音关联到对象。

Recap:

  • 在 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
}

后续步骤

我们介绍了 UWP 框架、图形、控件、用户界面和 Windows 10 游戏的音频。 本教程的下一部分是扩展示例游戏,其中说明了在开发游戏时可以使用的其他选项。

音频概念

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