Поделиться через


Добавление звука в пример Marble Maze

В этом документе описаны основные методики, которые следует учитывать при работе с звуком и показывает, как Marble Maze применяет эти методики. Marble Maze использует Microsoft Media Foundation для загрузки звуковых ресурсов из файлов и XAudio2 для смешивания и воспроизведения звука и применения эффектов к звуку.

Marble Maze играет музыку в фоновом режиме, а также использует звуки игрового процесса для обозначения игровых событий, таких как когда мрамор попадает на стену. Важной частью реализации является то, что Marble Maze использует реверверб или эхо, эффект для имитации звука мрамора при отскоке. Реализация эффектов реверб приводит к тому, что эхо достигает вас быстрее и громко в небольших комнатах; эхо тише и добраться до вас медленнее в более крупных комнатах.

Примечание.

Пример кода, соответствующий этому документу, найден в примере игры DirectX Marble Maze.

Ниже приведены некоторые ключевые моменты, которые обсуждаются в этом документе при работе с звуком в игре:

  • Рекомендуется использовать Media Foundation для декодирования звуковых ресурсов и XAudio2 для воспроизведения звука. Однако если у вас есть существующий механизм загрузки звуковых ресурсов, который работает в приложении универсальная платформа Windows (UWP), его можно использовать.

  • Звуковой граф содержит один исходный голос для каждого активного звука, ноль или несколько вложенных голосов и один главный голос. Исходные голоса могут передаваться в подмиксные голоса и (или) главный голос. Подмиксные голосовые каналы в другие подмиксовые голоса или главный голос.

  • Если фоновые музыкальные файлы большие, рассмотрите возможность потоковой передачи музыки в меньшие буферы, чтобы использовать меньше памяти.

  • Если это имеет смысл сделать, приостанавливайте воспроизведение звука, когда приложение теряет фокус или видимость или приостановлено. Возобновление воспроизведения, когда приложение восстанавливает фокус, становится видимым или возобновляется.

  • Задайте категории звука, чтобы отразить роль каждого звука. Например, вы обычно используете AudioCategory_GameMedia для фонового звука игры и AudioCategory_GameEffects для звуковых эффектов.

  • Обработка изменений устройства, включая наушники, путем освобождения и повторного воспроизведения всех звуковых ресурсов и интерфейсов.

  • Рассмотрите необходимость сжатия звуковых файлов при минимизации затрат на дисковое пространство и потоковую передачу. В противном случае вы можете оставить звук несжатыми, чтобы он загружался быстрее.

Знакомство с XAudio2 и Microsoft Media Foundation

XAudio2 — это низкоуровневая звуковая библиотека для Windows, которая специально поддерживает звук игры. Он обеспечивает цифровую обработку сигналов (DSP) и подсистему аудио-графа для игр. XAudio2 расширяет свои предшественники, DirectSound и XAudio, поддерживая тенденции вычислений, такие как архитектуры с плавающей запятой SIMD и звук HD. Она также поддерживает более сложные требования к обработке звука в современных играх.

В документе XAudio2 Key Concepts описываются основные понятия для использования XAudio2. Вкратце, основные понятия:

  • Интерфейс IXAudio2 является ядром подсистемы XAudio2. Marble Maze использует этот интерфейс для создания голосов и получения уведомлений при изменении или сбое выходного устройства.

  • Голосовая обработка, корректировка и воспроизведение звуковых данных.

  • Исходный голос — это коллекция аудиоканалов (mono, 5.1 и т. д.) и представляет один поток звуковых данных. В XAudio2 исходный голос — это место, где начинается обработка звука. Как правило, звуковые данные загружаются из внешнего источника, например файла или сети, и отправляются в исходный голос. Marble Maze использует Media Foundation для загрузки звуковых данных из файлов. Media Foundation представлен далее в этом документе.

  • Вложенная голосовая связь обрабатывает звуковые данные. Эта обработка может включать изменение звукового потока или объединение нескольких потоков в один. Marble Maze использует подмиксы для создания эффекта ревербации.

  • Главный голос объединяет данные из исходных и вложенных голосов и отправляет эти данные в звуковое оборудование.

  • Звуковой граф содержит один исходный голос для каждого активного звука, ноль или несколько вложенных голосов и только один образец.

  • Обратный вызов сообщает коду клиента, что некоторые события произошли в голосовой связи или в объекте обработчика. С помощью обратных вызовов можно повторно использовать память при завершении XAudio2 с буфером, реагировать на изменения звукового устройства (например, при подключении или отключении наушников) и т. д. Обработка наушников и изменений устройств далее в этом документе объясняет, как Marble Maze использует этот механизм для обработки изменений устройства.

Marble Maze использует два звуковых модуля (иными словами, два объекта IXAudio2 ) для обработки звука. Один двигатель обрабатывает фоновую музыку, а другой двигатель обрабатывает звуки игрового процесса.

Marble Maze также должен создать один главный голос для каждого двигателя. Помните, что подсистема управления объединяет аудиопотоки в один поток и отправляет этот поток в звуковое оборудование. Фоновый музыкальный поток, исходный голос, выводит данные на главный голос и два вложенных голоса. Подмиксные голоса выполняют эффект ревербации.

Media Foundation — это мультимедийная библиотека, которая поддерживает множество форматов аудио и видео. XAudio2 и Media Foundation дополняют друг друга. Marble Maze использует Media Foundation для загрузки звуковых ресурсов из файлов и использует XAudio2 для воспроизведения звука. Вам не нужно использовать Media Foundation для загрузки звуковых ресурсов. Если у вас есть существующий механизм загрузки звуковых ресурсов, который работает в приложениях универсальная платформа Windows (UWP), используйте его. Аудио, видео и камера обсуждают несколько способов реализации звука в приложении UWP.

Дополнительные сведения о XAudio2 см . в руководстве по программированию. Дополнительные сведения о Media Foundation см. в разделе Microsoft Media Foundation.

Инициализация звуковых ресурсов

Marble Mazes использует файл звука Windows Media (.wma) для фоновой музыки и файлов WAV (.wav) для звуков игрового процесса. Эти форматы поддерживаются Media Foundation. Хотя формат файла .wav изначально поддерживается XAudio2, игра должна проанализировать формат файла вручную, чтобы заполнить соответствующие структуры данных XAudio2. Marble Maze использует Media Foundation для более простой работы с файлами .wav. Полный список форматов мультимедиа, поддерживаемых Media Foundation, см. в разделе "Поддерживаемые форматы мультимедиа" в Media Foundation. Marble Maze не использует отдельные форматы звука во время разработки и времени выполнения и не использует поддержку сжатия XAudio2 ADPCM. Дополнительные сведения о сжатие ADPCM в XAudio2 см. в обзоре ADPCM.

Метод Audio::CreateResources, который вызывается из MarbleMazeMain::LoadDeferredResources, загружает аудиопотоки из файлов, инициализирует объекты ядра XAudio2 и создает исходные, вложенные и эталонные голоса.

Создание обработчиков XAudio2

Напомним, что Marble Maze создает один объект IXAudio2 для представления каждого звукового модуля, который он использует. Чтобы создать звуковой модуль, вызовите метод XAudio2Create . В следующем примере показано, как Marble Maze создает звуковой механизм, обрабатывающий фоновую музыку.

// In Audio.h
class Audio
{
private:
    IXAudio2*                   m_musicEngine;
// ...
}

// In Audio.cpp
void Audio::CreateResources()
{
    try
    {
        // ...
        DX::ThrowIfFailed(
            XAudio2Create(&m_musicEngine)
            );
        // ...
    }
    // ...
}

Marble Maze выполняет аналогичный шаг, чтобы создать звуковой механизм, который играет звуки игрового процесса.

Работа с интерфейсом IXAudio2 в приложении UWP отличается от классического приложения двумя способами. Во-первых, вам не нужно вызывать CoInitializeEx перед вызовом XAudio2Create. Кроме того, IXAudio2 больше не поддерживает перечисление устройств. Сведения о перечислении звуковых устройств см. в разделе "Перечисление устройств".

Создание эталонных голосов

В следующем примере показано, как метод Audio::CreateResources создает образец для фоновой музыки с помощью метода IXAudio2::CreateMasteringVoice . В этом примере m_musicMasteringVoice является объектом IXAudio2MasteringVoice . Мы указываем два входных канала; это упрощает логику для эффекта ревербации.

Мы указываем 48000 в качестве входной частоты выборки. Мы выбрали эту частоту выборки, так как она представляла баланс между качеством звука и объемом требуемой обработки ЦП. Более высокая скорость выборки потребует больше обработки ЦП без заметного преимущества качества.

Наконец, мы указываем AudioCategory_GameMedia в качестве категории аудиопотока, чтобы пользователи могли прослушивать музыку из другого приложения, когда они играют в игру. При воспроизведении музыкального приложения Windows отключает все голоса, созданные параметром AudioCategory_GameMedia . Пользователь по-прежнему слышит звуки игрового процесса, так как они создаются параметром AudioCategory_GameEffects . Дополнительные сведения о категориях аудио см. в AUDIO_STREAM_CATEGORY.

// This sample plays the equivalent of background music, which we tag on the  
// mastering voice as AudioCategory_GameMedia. In ordinary usage, if we were  
// playing the music track with no effects, we could route it entirely through 
// Media Foundation. Here, we are using XAudio2 to apply a reverb effect to the 
// music, so we use Media Foundation to decode the data then we feed it through 
// the XAudio2 pipeline as a separate Mastering Voice, so that we can tag it 
// as Game Media. We default the mastering voice to 2 channels to simplify  
// the reverb logic.
DX::ThrowIfFailed(
    m_musicEngine->CreateMasteringVoice(
        &m_musicMasteringVoice,
        2,
        48000,
        0,
        nullptr,
        nullptr,
        AudioCategory_GameMedia
        )
);

Метод Audio::CreateResources выполняет аналогичный шаг, чтобы создать главный голос для звуков игрового процесса, за исключением того, что он указывает AudioCategory_GameEffects для параметра StreamCategory , который является значением по умолчанию.

Создание эффекта ревербации

Для каждого голоса можно использовать XAudio2 для создания последовательностей эффектов, обрабатывающих звук. Такая последовательность называется цепочкой эффектов. Используйте цепочки эффектов, если вы хотите применить один или несколько эффектов к голосу. Цепочки эффектов могут быть разрушительными; То есть каждый эффект в цепочке может перезаписать звуковой буфер. Это свойство важно, так как XAudio2 не гарантирует, что выходные буферы инициализированы молчанием. Объекты эффектов представлены в XAudio2 кроссплатформенными объектами обработки звука (XAPO). Дополнительные сведения о XAPO см. в обзоре XAPO.

При создании цепочки эффектов выполните следующие действия.

  1. Создайте объект эффекта.

  2. Заполните структуру XAUDIO2_EFFECT_DESCRIPTOR данными эффектов.

  3. Заполните структуру XAUDIO2_EFFECT_CHAIN данными.

  4. Примените цепочку эффектов к голосу.

  5. Заполните структуру параметров эффекта и примените ее к эффекту.

  6. Отключите или включите эффект при необходимости.

Класс Audio определяет метод CreateReverb для создания цепочки эффектов, реализующей реверб. Этот метод вызывает метод XAudio2CreateReverb для создания объекта ComPtr<IUnknown> , soundEffectXAPO, который выступает в качестве подмиксного голоса для эффекта ревербации.

Microsoft::WRL::ComPtr<IUnknown> soundEffectXAPO;

DX::ThrowIfFailed(
    XAudio2CreateReverb(&soundEffectXAPO)
    );

Структура XAUDIO2_EFFECT_DESCRIPTOR содержит сведения о XAPO для использования в цепочке эффектов, например целевое число выходных каналов. Метод Audio::CreateReverb создает объект XAUDIO2_EFFECT_DESCRIPTOR, звукEffectdescriptor, который имеет отключенное состояние, использует два выходных канала и ссылается на soundEffectXAPO для эффекта ревербации. soundEffectdescriptor начинается в отключенном состоянии, так как игра должна задать параметры, прежде чем эффект начнет изменять звуки игры. Marble Maze использует два выходных канала для упрощения логики эффекта ревербации.

soundEffectdescriptor.InitialState = false;
soundEffectdescriptor.OutputChannels = 2;
soundEffectdescriptor.pEffect = soundEffectXAPO.Get();

Если цепочка эффектов имеет несколько эффектов, каждый эффект требует объекта. Структура XAUDIO2_EFFECT_CHAIN содержит массив объектов XAUDIO2_EFFECT_DESCRIPTOR, участвующих в действии. В следующем примере показано, как метод Audio::CreateReverb указывает один эффект для реализации ревербации.

XAUDIO2_EFFECT_CHAIN soundEffectChain;

// ...

soundEffectChain.EffectCount = 1;
soundEffectChain.pEffectDescriptors = &soundEffectdescriptor;

Метод Audio::CreateReverb вызывает метод IXAudio2::CreateSubmixVoice для создания подмиксов для эффекта. Он задает объект XAUDIO2_EFFECT_CHAIN, soundEffectChain, для параметра pEffectChain, чтобы связать цепочку эффектов с голосом. Marble Maze также указывает два выходных канала и частоту выборки 48 килогерц.

DX::ThrowIfFailed(
    engine->CreateSubmixVoice(newSubmix, 2, 48000, 0, 0, nullptr, &soundEffectChain)
    );

Совет

Если вы хотите подключить существующую цепочку эффектов к существующему подмиксу или заменить текущую цепочку эффектов, используйте метод IXAudio2Voice::SetEffectChain .

Метод Audio::CreateReverb вызывает IXAudio2Voice::SetEffectParameters , чтобы задать дополнительные параметры, связанные с эффектом. Этот метод принимает структуру параметров, относящуюся к эффекту. Объект XAUDIO2FX_REVERB_PARAMETERS , m_reverbParametersSmall, содержащий параметры эффекта для реверверации, инициализируется в методе Audio::Initialize, так как каждый эффект реверверации использует одинаковые параметры. В следующем примере показано, как метод Audio::Initialize инициализирует параметры ревербации для реверберации близкого поля.

m_reverbParametersSmall.ReflectionsDelay = XAUDIO2FX_REVERB_DEFAULT_REFLECTIONS_DELAY;
m_reverbParametersSmall.ReverbDelay = XAUDIO2FX_REVERB_DEFAULT_REVERB_DELAY;
m_reverbParametersSmall.RearDelay = XAUDIO2FX_REVERB_DEFAULT_REAR_DELAY;
m_reverbParametersSmall.PositionLeft = XAUDIO2FX_REVERB_DEFAULT_POSITION;
m_reverbParametersSmall.PositionRight = XAUDIO2FX_REVERB_DEFAULT_POSITION;
m_reverbParametersSmall.PositionMatrixLeft = XAUDIO2FX_REVERB_DEFAULT_POSITION_MATRIX;
m_reverbParametersSmall.PositionMatrixRight = XAUDIO2FX_REVERB_DEFAULT_POSITION_MATRIX;
m_reverbParametersSmall.EarlyDiffusion = 4;
m_reverbParametersSmall.LateDiffusion = 15;
m_reverbParametersSmall.LowEQGain = XAUDIO2FX_REVERB_DEFAULT_LOW_EQ_GAIN;
m_reverbParametersSmall.LowEQCutoff = XAUDIO2FX_REVERB_DEFAULT_LOW_EQ_CUTOFF;
m_reverbParametersSmall.HighEQGain = XAUDIO2FX_REVERB_DEFAULT_HIGH_EQ_GAIN;
m_reverbParametersSmall.HighEQCutoff = XAUDIO2FX_REVERB_DEFAULT_HIGH_EQ_CUTOFF;
m_reverbParametersSmall.RoomFilterFreq = XAUDIO2FX_REVERB_DEFAULT_ROOM_FILTER_FREQ;
m_reverbParametersSmall.RoomFilterMain = XAUDIO2FX_REVERB_DEFAULT_ROOM_FILTER_MAIN;
m_reverbParametersSmall.RoomFilterHF = XAUDIO2FX_REVERB_DEFAULT_ROOM_FILTER_HF;
m_reverbParametersSmall.ReflectionsGain = XAUDIO2FX_REVERB_DEFAULT_REFLECTIONS_GAIN;
m_reverbParametersSmall.ReverbGain = XAUDIO2FX_REVERB_DEFAULT_REVERB_GAIN;
m_reverbParametersSmall.DecayTime = XAUDIO2FX_REVERB_DEFAULT_DECAY_TIME;
m_reverbParametersSmall.Density = XAUDIO2FX_REVERB_DEFAULT_DENSITY;
m_reverbParametersSmall.RoomSize = XAUDIO2FX_REVERB_DEFAULT_ROOM_SIZE;
m_reverbParametersSmall.WetDryMix = XAUDIO2FX_REVERB_DEFAULT_WET_DRY_MIX;
m_reverbParametersSmall.DisableLateField = TRUE;

В этом примере используются значения по умолчанию для большинства параметров реверберации, но при этом параметр DisableLateField имеет значение TRUE, чтобы указать реверб вблизи поля, EarlyDiffusion значение 4, чтобы имитировать плоские поверхности рядом с поверхностями, а в концеdiffusion — 15, чтобы имитировать очень диффузные удаленные поверхности. Плоские вблизи поверхностей вызывают эхо, чтобы достичь вас быстрее и громко; диффузные отдаленные поверхности вызывают эхо быть тише и достичь вас более медленно. Вы можете поэкспериментировать со значениями ревербации, чтобы получить нужный эффект в игре или использовать метод ReverbConvertI3DL2ToNative для использования параметров I3DL2 (интерактивный уровень 3D-визуализации звука уровня 2.0).

В следующем примере показано, как Audio::CreateReverb задает параметры ревербации. newSubmix — это объект IXAudio2SubmixVoice**. параметры — это объект XAUDIO2FX_REVERB_PARAMETERS*.

DX::ThrowIfFailed(
    (*newSubmix)->SetEffectParameters(0, parameters, sizeof(m_reverbParametersSmall))
    );

Метод Audio::CreateReverb завершается, включив эффект с помощью IXAudio2Voice::EnableEffect, если установлен флаг enableEffect. Он также задает свой том с помощью матрицы вывода IXAudio2Voice::SetVolume и выходной матрицы с помощью IXAudio2Voice::SetOutputMatrix. Эта часть задает для тома полную (1.0), а затем указывает матрицу тома, чтобы быть молчанием для левых и правых входных данных, а также для левых и правых динамиков вывода. Это связано с тем, что другой код позже исчезает между двумя реверберами (имитация перехода от того, что находится рядом с стеной в большой комнате) или при необходимости отключает обе реверберации. После отмены пути ревербации игра задает матрицу {1.0f, 0.0f, 0.0f, 1.0f}, чтобы перенаправить выходные данные левого реверба на левый вход голосовой передачи и правый реверб в правый вход голосовой передачи.

if (enableEffect)
{
    DX::ThrowIfFailed(
        (*newSubmix)->EnableEffect(0)
        );    
}

DX::ThrowIfFailed(
    (*newSubmix)->SetVolume (1.0f)
    );

float outputMatrix[4] = {0, 0, 0, 0};
DX::ThrowIfFailed(
    (*newSubmix)->SetOutputMatrix(masteringVoice, 2, 2, outputMatrix)
    );

Marble Maze вызывает метод Audio::CreateReverb четыре раза: два раза для фоновой музыки и два раза для звуков игрового процесса. Ниже показано, как Marble Maze вызывает метод CreateReverb для фоновой музыки.

CreateReverb(
    m_musicEngine, 
    m_musicMasteringVoice, 
    &m_reverbParametersSmall, 
    &m_musicReverbVoiceSmallRoom, 
    true
    );
CreateReverb(
    m_musicEngine, 
    m_musicMasteringVoice, 
    &m_reverbParametersLarge, 
    &m_musicReverbVoiceLargeRoom, 
    true
    );

Список возможных источников эффектов для использования с XAudio2 см. в статье XAudio2 Audio Effects.

Загрузка звуковых данных из файла

Marble Maze определяет класс MediaStreamer , который использует Media Foundation для загрузки звуковых ресурсов из файлов. Marble Maze использует один объект MediaStreamer для загрузки каждого звукового файла.

Marble Maze вызывает метод MediaStreamer::Initialize для инициализации каждого звукового потока. Вот как метод Audio::CreateResources вызывает MediaStreamer::Initialize для инициализации аудиопотока для фоновой музыки:

// Media Foundation is a convenient way to get both file I/O and format decode for 
// audio assets. You can replace the streamer in this sample with your own file I/O 
// and decode routines.
m_musicStreamer.Initialize(L"Media\\Audio\\background.wma");

Метод MediaStreamer::Initialize начинается с вызова метода MFStartup для инициализации Media Foundation. MF_VERSION — это макрос, определенный в mfapi.h, и это то, что мы указываем в качестве используемой версии Media Foundation.

DX::ThrowIfFailed(
    MFStartup(MF_VERSION)
    );

MediaStreamer::Initialize затем вызывает MFCreateSourceReaderFromURL для создания объекта IMFSourceReader . Объект IMFSourceReader, m_reader, считывает данные мультимедиа из файла, указанного по URL-адресу.

DX::ThrowIfFailed(
    MFCreateSourceReaderFromURL(url, nullptr, &m_reader)
    );

Метод MediaStreamer::Initialize затем создает объект IMFMediaType с помощью MFCreateMediaType для описания формата аудиопотока. Формат звука имеет два типа: основной тип и подтип. Основной тип определяет общий формат носителя, например видео, аудио, скрипт и т. д. Подтип определяет формат, например PCM, ADPCM или WMA.

Метод MediaStreamer::Initialize использует метод IMFAttributes::SetGUID, чтобы указать основной тип (MF_MT_MAJOR_TYPE) в качестве звука (MFMediaType_Audio) и дополнительный тип (MF_MT_SUBTYPE) в качестве несжатого звука PCM (MFAudioFormat_PCM). MF_MT_MAJOR_TYPE и MF_MT_SUBTYPE — атрибуты Media Foundation. MFMediaType_Audio и MFAudioFormat_PCM являются типами и подтипами GUID; дополнительные сведения см. в разделе "Типы аудиомедий". Метод IMFSourceReader::SetCurrentMediaType связывает тип носителя с средством чтения потока.

// Set the decoded output format as PCM. 
// XAudio2 on Windows can process PCM and ADPCM-encoded buffers. 
// When this sample uses Media Foundation, it always decodes into PCM.

DX::ThrowIfFailed(
    MFCreateMediaType(&mediaType)
    );

DX::ThrowIfFailed(
    mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)
    );

DX::ThrowIfFailed(
    mediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)
    );

DX::ThrowIfFailed(
    m_reader->SetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, mediaType.Get())
    );

Метод MediaStreamer::Initialize затем получает полный формат выходных носителей из Media Foundation с помощью МВФSourceReader::GetCurrentMediaType и вызывает метод MFCreateWaveFormatExFromMMediaType для преобразования типа звукового носителя Media Foundation в структуру WAVEFORMATEX. Структура WAVEFORMATEX определяет формат звуковых данных волны. Marble Maze использует эту структуру для создания исходных голосов и применения низкопроходного фильтра к мраморного скользящего звука.

// Get the complete WAVEFORMAT from the Media Type.
DX::ThrowIfFailed(
    m_reader->GetCurrentMediaType(MF_SOURCE_READER_FIRST_AUDIO_STREAM, &outputMediaType)
    );

uint32 formatSize = 0;
WAVEFORMATEX* waveFormat;
DX::ThrowIfFailed(
    MFCreateWaveFormatExFromMFMediaType(outputMediaType.Get(), &waveFormat, &formatSize)
    );
CopyMemory(&m_waveFormat, waveFormat, sizeof(m_waveFormat));
CoTaskMemFree(waveFormat);

Внимание

Метод MFCreateWaveFormatExFromMFMediaType использует CoTaskMemAlloc для выделения объекта WAVEFORMATEX . Поэтому убедитесь, что при завершении работы с этим объектом необходимо вызвать CoTaskMemFree .

 

Метод MediaStreamer::Initialize завершается путем вычисления длины потока, m_maxStreamLengthInBytes в байтах. Для этого он вызывает метод IMFSourceReader::GetPresentationAttribute , чтобы получить длительность звукового потока в 100-наносекундах, преобразует длительность в разделы, а затем умножается на среднюю скорость передачи данных в байтах в секунду. Marble Maze позже использует это значение для выделения буфера, который содержит каждый звук игрового процесса.

// Get the total length of the stream, in bytes.
PROPVARIANT var;
DX::ThrowIfFailed(
    m_reader->
        GetPresentationAttribute(MF_SOURCE_READER_MEDIASOURCE, MF_PD_DURATION, &var)
    );

// duration is in 100ns units; convert to seconds, and round up
// to the nearest whole byte.
ULONGLONG duration = var.uhVal.QuadPart;
m_maxStreamLengthInBytes =
    static_cast<unsigned int>(
        ((duration * static_cast<ULONGLONG>(m_waveFormat.nAvgBytesPerSec)) + 10000000)
        / 10000000
        );

Создание исходных голосов

Marble Maze создает исходные голоса XAudio2, чтобы играть каждую из своих игр звуки и музыку в исходных голосах. Класс Audio определяет объект IXAudio2SourceVoice для фоновой музыки и массив объектов SoundEffectData для хранения звуков игрового процесса. Структура SoundEffectData содержит объект IXAudio2SourceVoice для эффекта, а также определяет другие связанные с эффектом данные, такие как звуковой буфер. Audio.h определяет перечисление SoundEvent . Marble Maze использует это перечисление для идентификации каждого звука игрового процесса. Класс Audio также использует это перечисление для индексирования массива объектов SoundEffectData .

enum SoundEvent
{
    RollingEvent        = 0,
    FallingEvent        = 1,
    CollisionEvent      = 2,
    CheckpointEvent     = 3,
    MenuChangeEvent     = 4,
    MenuSelectedEvent   = 5,
    LastSoundEvent,
};

В следующей таблице показаны связи между каждым из этих значений, файлом, содержащими связанные звуковые данные, и кратким описанием того, что представляет каждый звук. Звуковые файлы находятся в папке \Media\Audio .

Значение SoundEvent Имя файла Description
RollingEvent MarbleRoll.wav Играл в качестве мраморных рулонов.
ПадениеEvent MarbleFall.wav Играл, когда мрамор падает с лабиринта.
CollisionEvent MarbleHit.wav Играл, когда мрамор столкнулся с лабиринтом.
Контрольная точкаEvent Checkpoint.wav Играл, когда мрамор проходит через контрольную точку.
MenuChangeEvent MenuChange.wav Воспроизводится, когда пользователь изменяет текущий пункт меню.
MenuSelectedEvent MenuSelect.wav Воспроизводится, когда пользователь выбирает пункт меню.

 

В следующем примере показано, как метод Audio::CreateResources создает исходный голос для фоновой музыки. Структура XAUDIO2_SEND_DESCRIPTOR определяет целевой целевой голос назначения из другого голоса и указывает, следует ли использовать фильтр. Marble Maze вызывает метод Audio::SetSoundEffectFilter , чтобы использовать фильтры для изменения звука мяча по мере его свертки. Структура XAUDIO2_VOICE_SENDS определяет набор голосов для получения данных из одного выходного голоса. Marble Maze отправляет данные из исходного голоса в освояющий голос (для сухого или незатерянной, части воспроизведения звука) и к двум подмиксным голосам, реализующим мокрый или реверберантный, часть воспроизведения звука.

Метод IXAudio2::CreateSourceVoice создает и настраивает исходный голос. Она принимает структуру WAVEFORMATEX , которая определяет формат звуковых буферов, отправляемых голосу. Как упоминалось ранее, Marble Maze использует формат PCM.

XAUDIO2_SEND_DESCRIPTOR descriptors[3];
descriptors[0].pOutputVoice = m_musicMasteringVoice;
descriptors[0].Flags = 0;
descriptors[1].pOutputVoice = m_musicReverbVoiceSmallRoom;
descriptors[1].Flags = 0;
descriptors[2].pOutputVoice = m_musicReverbVoiceLargeRoom;
descriptors[2].Flags = 0;
XAUDIO2_VOICE_SENDS sends = {0};
sends.SendCount = 3;
sends.pSends = descriptors;
WAVEFORMATEX& waveFormat = m_musicStreamer.GetOutputWaveFormatEx();

DX::ThrowIfFailed(
    m_musicEngine->CreateSourceVoice(&m_musicSourceVoice, &waveFormat, 0, 1.0f, &m_voiceContext, &sends, nullptr)
    );

DX::ThrowIfFailed(
    m_musicMasteringVoice->SetVolume(0.4f)
    );

Воспроизведение фоновой музыки

Исходный голос создается в остановленном состоянии. Marble Maze начинает фоновую музыку в цикле игры. Первый вызов MarbleMazeMazeMain::Update вызывает Audio::Start , чтобы начать фоновую музыку.

if (!m_audio.m_isAudioStarted)
{
    m_audio.Start();
}

Метод Audio::Start вызывает IXAudio2SourceVoice::Start , чтобы начать обработку исходного голоса для фоновой музыки.

void Audio::Start()
{     
    if (m_engineExperiencedCriticalError)
    {
        return;
    }

    HRESULT hr = m_musicSourceVoice->Start(0);

    if SUCCEEDED(hr) {
        m_isAudioStarted = true;
    }
    else
    {
        m_engineExperiencedCriticalError = true;
    }
}

Исходный голос передает звуковые данные на следующий этап звукового графа. В случае Marble Maze следующий этап содержит два вложенных голоса, которые применяют два эффектов ревербации к звуку. Один из вложенных голосовых элементов применяет закрываемый реверб поля. второе применяет далекое обратное поле.

Количество, которое каждый вложенный голос вносит в окончательный набор, определяется размером и формой комнаты. Почти поле реверб способствует больше, когда мяч находится рядом с стеной или в небольшой комнате, и поздняя поле реверб способствует больше, когда мяч находится в большом пространстве. Этот метод создает более реалистичный эффект эхо, как мрамор перемещается через лабиринт. Дополнительные сведения о том, как Marble Maze реализует этот эффект, см. в разделе Audio::SetRoomSize и Физика::CalculateCurrentRoomSize в исходном коде Marble Maze.

Примечание.

В игре, в которой большинство размеров помещений относительно одинаковы, вы можете использовать более простую модель реверберации. Например, можно использовать один параметр ревербации для всех комнат или создать предварительно определенный параметр ревербации для каждой комнаты.

Метод Audio::CreateResources использует Media Foundation для загрузки фоновой музыки. Однако на этом этапе исходный голос не имеет звуковых данных для работы. Кроме того, поскольку фоновые циклы музыки, исходный голос должен регулярно обновляться с данными, чтобы музыка продолжала играть.

Чтобы сохранить исходный голос заполнен данными, игровой цикл обновляет звуковые буферы каждый кадр. Метод MarbleMazeMain::Render вызывает Audio::Render для обработки фонового звукового буфера музыки. Класс Audio определяет массив из трех звуковых буферов, m_audioBuffers. Каждый буфер содержит 64 КБ (65536 байт) данных. Цикл считывает данные из объекта Media Foundation и записывает эти данные в исходный голос, пока исходный голос не содержит три буфера очереди.

Внимание

Хотя Marble Maze использует буфер размером 64 КБ для хранения музыкальных данных, может потребоваться использовать более большой или меньший буфер. Эта сумма зависит от требований вашей игры.

// This sample processes audio buffers during the render cycle of the application.
// As long as the sample maintains a high-enough frame rate, this approach should
// not glitch audio. In game code, it is best for audio buffers to be processed
// on a separate thread that is not synced to the main render loop of the game.
void Audio::Render()
{
    if (m_engineExperiencedCriticalError)
    {
        m_engineExperiencedCriticalError = false;
        ReleaseResources();
        Initialize();
        CreateResources();
        Start();
        if (m_engineExperiencedCriticalError)
        {
            return;
        }
    }

    try
    {
        bool streamComplete;
        XAUDIO2_VOICE_STATE state;
        uint32 bufferLength;
        XAUDIO2_BUFFER buf = {0};

        // Use MediaStreamer to stream the buffers.
        m_musicSourceVoice->GetState(&state);
        while (state.BuffersQueued <= MAX_BUFFER_COUNT - 1)
        {
            streamComplete = m_musicStreamer.GetNextBuffer(
                m_audioBuffers[m_currentBuffer],
                STREAMING_BUFFER_SIZE,
                &bufferLength
                );

            if (bufferLength > 0)
            {
                buf.AudioBytes = bufferLength;
                buf.pAudioData = m_audioBuffers[m_currentBuffer];
                buf.Flags = (streamComplete) ? XAUDIO2_END_OF_STREAM : 0;
                buf.pContext = 0;
                DX::ThrowIfFailed(
                    m_musicSourceVoice->SubmitSourceBuffer(&buf)
                    );

                m_currentBuffer++;
                m_currentBuffer %= MAX_BUFFER_COUNT;
            }

            if (streamComplete)
            {
                // Loop the stream.
                m_musicStreamer.Restart();
                break;
            }

            m_musicSourceVoice->GetState(&state);
        }
    }
    catch (...)
    {
        m_engineExperiencedCriticalError = true;
    }
}

Цикл также обрабатывает, когда объект Media Foundation достигает конца потока. В этом случае вызывается метод IMFSourceReader::SetCurrentPosition , чтобы сбросить положение источника звука.

void MediaStreamer::Restart()
{
    if (m_reader == nullptr)
    {
        return;
    }

    PROPVARIANT var = {0};
    var.vt = VT_I8;

    DX::ThrowIfFailed(
        m_reader->SetCurrentPosition(GUID_NULL, var)
        );
}

Чтобы реализовать циклы звука для одного буфера (или для всего звука, полностью загруженного в память), можно задать для поля XAUDIO2_BUFFER::LoopCount XAUDIO2_LOOP_INFINITE при инициализации звука. Marble Maze использует этот метод для воспроизведения скользящего звука для мрамора.

if (sound == RollingEvent)
{
    m_soundEffects[sound].m_audioBuffer.LoopCount = XAUDIO2_LOOP_INFINITE;
}

Однако для фоновой музыки Marble Maze управляет буферами напрямую, чтобы лучше контролировать объем используемой памяти. Если музыкальные файлы большие, вы можете передавать данные музыки в меньшие буферы. Это может помочь сбалансировать размер памяти с частотой способности игры обрабатывать и передавать звуковые данные.

Совет

Если ваша игра имеет низкую или переменную частоту кадров, обработка звука в основном потоке может привести к непредвиденным паузам или всплывкам в звуке, так как звуковой механизм имеет недостаточно буферных звуковых данных для работы. Если ваша игра учитывает эту проблему, рассмотрите возможность обработки звука в отдельном потоке, который не выполняет отрисовку. Этот подход особенно полезен на компьютерах с несколькими процессорами, так как ваша игра может использовать бездействующие процессоры.

Реагирование на события игры

Класс Audio предоставляет такие методы, как PlaySoundEffect, IsSoundEffectStarted, StopSoundEffect, SetSoundEffectVolume, SetSoundEffectPitch и SetSoundEffectFilter, чтобы обеспечить управление игрой при звуке и остановке, а также управлять звуковыми свойствами, такими как громкость и шаг. Например, если мрамор падает с лабиринта, MarbleMazeMain::Update вызывает метод Audio::P laySoundEffect для воспроизведения звука FallingEvent .

m_audio.PlaySoundEffect(FallingEvent);

Метод Audio::P laySoundEffect вызывает метод IXAudio2SourceVoice::Start , чтобы начать воспроизведение звука. Если метод IXAudio2SourceVoice::Start уже вызван, он не запускается снова. Audio::P laySoundEffect затем выполняет пользовательскую логику для определенных звуков.

void Audio::PlaySoundEffect(SoundEvent sound)
{
    XAUDIO2_BUFFER buf = {0};
    XAUDIO2_VOICE_STATE state = {0};

    if (m_engineExperiencedCriticalError)
    {
        // If there's an error, then we'll recreate the engine on the next
        // render pass.
        return;
    }

    SoundEffectData* soundEffect = &m_soundEffects[sound];
    HRESULT hr = soundEffect->m_soundEffectSourceVoice->Start();

    if FAILED(hr)
    {
        m_engineExperiencedCriticalError = true;
        return;
    }

    // For one-off voices, submit a new buffer if there's none queued up,
    // and allow up to two collisions to be queued up. 
    if (sound != RollingEvent)
    {
        XAUDIO2_VOICE_STATE state = {0};

        soundEffect->m_soundEffectSourceVoice->
            GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED);

        if (state.BuffersQueued == 0)
        {
            soundEffect->m_soundEffectSourceVoice->
                SubmitSourceBuffer(&soundEffect->m_audioBuffer);
        }
        else if (state.BuffersQueued < 2 && sound == CollisionEvent)
        {
            soundEffect->m_soundEffectSourceVoice->
                SubmitSourceBuffer(&soundEffect->m_audioBuffer);
        }

        // For the menu clicks, we want to stop the voice and replay the click
        // right away.
        // Note that stopping and then flushing could cause a glitch due to the
        // waveform not being at a zero-crossing, but due to the nature of the 
        // sound (fast and 'clicky'), we don't mind.
        if (state.BuffersQueued > 0 && sound == MenuChangeEvent)
        {
            soundEffect->m_soundEffectSourceVoice->Stop();
            soundEffect->m_soundEffectSourceVoice->FlushSourceBuffers();

            soundEffect->m_soundEffectSourceVoice->
                SubmitSourceBuffer(&soundEffect->m_audioBuffer);

            soundEffect->m_soundEffectSourceVoice->Start();
        }
    }

    m_soundEffects[sound].m_soundEffectStarted = true;
}

Для звуков, отличных от прокатки, метод Audio::P laySoundEffect вызывает IXAudio2SourceVoice::GetState , чтобы определить количество буферов, которые воспроизводит исходный голос. Он вызывает IXAudio2SourceVoice::SubmitSourceBuffer , чтобы добавить звуковые данные для звука в очередь входных данных голоса, если буферы не активны. Метод Audio::P laySoundEffect также позволяет воспроизводить звук столкновения два раза в последовательности. Это происходит, например, когда мрамор сталкивается с углом лабиринта.

Как уже описано, класс Audio использует флаг XAUDIO2_LOOP_INFINITE при инициализации звука для скользящего события. Звук запускает циклическое воспроизведение при первом вызове Audio::P laySoundEffect для этого события. Чтобы упростить логику воспроизведения для скользящего звука, Marble Maze отключает звук вместо остановки. Как мрамор изменяет скорость, Marble Maze изменяет шаг и громкость звука, чтобы дать ему более реалистичный эффект. Ниже показано, как метод MarbleMazeMain::Update обновляет шаг и объем мрамора по мере изменения скорости и того, как он отключает звук, задав его громкость нулю при остановке мрамора.

// Play the roll sound only if the marble is actually rolling.
if (ci.isRollingOnFloor && volume > 0)
{
    if (!m_audio.IsSoundEffectStarted(RollingEvent))
    {
        m_audio.PlaySoundEffect(RollingEvent);
    }

    // Update the volume and pitch by the velocity.
    m_audio.SetSoundEffectVolume(RollingEvent, volume);
    m_audio.SetSoundEffectPitch(RollingEvent, pitch);

    // The rolling sound has at most 8000Hz sounds, so we linearly
    // ramp up the low-pass filter the faster we go.
    // We also reduce the Q-value of the filter, starting with a
    // relatively broad cutoff and get progressively tighter.
    m_audio.SetSoundEffectFilter(
        RollingEvent,
        600.0f + 8000.0f * volume,
        XAUDIO2_MAX_FILTER_ONEOVERQ - volume*volume
        );
}
else
{
    m_audio.SetSoundEffectVolume(RollingEvent, 0);
}

Реагирование на события приостановки и возобновления

Структура приложения Marble Maze описывает, как Marble Maze поддерживает приостановку и возобновление работы. Когда игра приостановлена, игра приостанавливает звук. Когда игра возобновляется, игра возобновляет звук, где он остался. Мы делаем это, чтобы следовать рекомендациям по не использованию ресурсов, когда вы знаете, что они не нужны.

Метод Audio::SuspendAudio вызывается при приостановке игры. Этот метод вызывает метод IXAudio2::StopEngine , чтобы остановить весь звук. Хотя IXAudio2::StopEngine немедленно останавливает все выходные данные звука, он сохраняет звуковой граф и его параметры эффекта (например, эффект ревербации, применяемый при отскоках мрамора).

// Uses the IXAudio2::StopEngine method to stop all audio immediately.  
// It leaves the audio graph untouched, which preserves all effect parameters   
// and effect histories (like reverb effects) voice states, pending buffers,  
// cursor positions and so on. 
// When the engines are restarted, the resulting audio will sound as if it had  
// never been stopped except for the period of silence. 
void Audio::SuspendAudio()
{
    if (m_engineExperiencedCriticalError)
    {
        return;
    }

    if (m_isAudioStarted)
    {
        m_musicEngine->StopEngine();
        m_soundEffectEngine->StopEngine();
    }

    m_isAudioStarted = false;
}

Метод Audio::ResumeAudio вызывается при возобновлении игры. Этот метод использует метод IXAudio2::StartEngine для перезапуска звука. Так как вызов IXAudio2::StopEngine сохраняет звуковой граф и его параметры эффекта, выходные данные звука возобновляются, где он остался.

// Restarts the audio streams. A call to this method must match a previous call
// to SuspendAudio. This method causes audio to continue where it left off.
// If there is a problem with the restart, the m_engineExperiencedCriticalError
// flag is set. The next call to Render will recreate all the resources and
// reset the audio pipeline.
void Audio::ResumeAudio()
{
    if (m_engineExperiencedCriticalError)
    {
        return;
    }

    HRESULT hr = m_musicEngine->StartEngine();
    HRESULT hr2 = m_soundEffectEngine->StartEngine();

    if (FAILED(hr) || FAILED(hr2))
    {
        m_engineExperiencedCriticalError = true;
    }
}

Обработка изменений наушников и устройств

Marble Maze использует обратные вызовы двигателя для обработки сбоев двигателя XAudio2, таких как при изменении звукового устройства. Вероятно, причина изменения устройства заключается в том, что пользователь игры подключает или отключает наушники. Рекомендуется реализовать обратный вызов обработчика, который обрабатывает изменения устройства. В противном случае игра перестанет воспроизводить звук, когда пользователь подключается или удаляет наушники, пока игра не будет перезапущена.

Audio.h определяет класс AudioEngineCallbacks . Этот класс реализует интерфейс IXAudio2EngineCallback .

class AudioEngineCallbacks: public IXAudio2EngineCallback
{
private:
    Audio* m_audio;

public :
    AudioEngineCallbacks(){};
    void Initialize(Audio* audio);

    // Called by XAudio2 just before an audio processing pass begins.
    void _stdcall OnProcessingPassStart(){};

    // Called just after an audio processing pass ends.
    void  _stdcall OnProcessingPassEnd(){};

    // Called when a critical system error causes XAudio2
    // to be closed and restarted. The error code is given in Error.
    void  _stdcall OnCriticalError(HRESULT Error);
};

Интерфейс IXAudio2EngineCallback позволяет получать уведомления о возникновении событий обработки звука и при возникновении критической ошибки подсистемы. Чтобы зарегистрировать обратные вызовы, Marble Maze вызывает метод IXAudio2::RegisterForCallbacks в Audio::CreateResources, после создания объекта IXAudio2 для подсистемы музыки.

m_musicEngineCallback.Initialize(this);
m_musicEngine->RegisterForCallbacks(&m_musicEngineCallback);

Marble Maze не требует уведомления при запуске или завершении обработки звука. Поэтому он реализует методы IXAudio2EngineCallback::OnProcessingPassStart и IXAudio2EngineCallback::OnProcessingPassEnd , чтобы ничего не делать. Для метода IXAudio2EngineCallback::OnCriticalError Marble Maze вызывает метод SetEngineExperiencedCriticalError, который задает флаг m_engineExperiencedCriticalError.

// Audio.cpp

// Called when a critical system error causes XAudio2 
// to be closed and restarted. The error code is given in Error. 
void  _stdcall AudioEngineCallbacks::OnCriticalError(HRESULT Error)
{
    m_audio->SetEngineExperiencedCriticalError();
}
// Audio.h (Audio class)

// This flag can be used to tell when the audio system 
// is experiencing critical errors.
// XAudio2 gives a critical error when the user unplugs
// the headphones and a new speaker configuration is generated.
void SetEngineExperiencedCriticalError()
{
    m_engineExperiencedCriticalError = true;
}

При возникновении критической ошибки обработка звука останавливается и все дополнительные вызовы XAudio2 завершаются ошибкой. Чтобы восстановить эту ситуацию, необходимо освободить экземпляр XAudio2 и создать новый. Метод Audio::Render , который вызывается из цикла игры каждый кадр, сначала проверяет флаг m_engineExperiencedCriticalError . Если этот флаг задан, он очищает флаг, освобождает текущий экземпляр XAudio2, инициализирует ресурсы, а затем запускает фоновую музыку.

if (m_engineExperiencedCriticalError)
{
    m_engineExperiencedCriticalError = false;
    ReleaseResources();
    Initialize();
    CreateResources();
    Start();
    if (m_engineExperiencedCriticalError)
    {
        return;
    }
}

Marble Maze также использует флаг m_engineExperiencedCriticalError для защиты от вызова в XAudio2, если звуковое устройство недоступно. Например, метод MarbleMazeMain::Update не обрабатывает звук для событий скользящего или столкновения при установке этого флага. Приложение пытается восстановить звуковой механизм каждый кадр, если это необходимо; Однако флаг m_engineExperiencedCriticalError всегда может быть установлен, если на компьютере нет звукового устройства или наушников нет другого доступного звукового устройства.

Внимание

Как правило, не выполняйте блокирующие операции в теле обратного вызова обработчика. Это может привести к проблемам с производительностью. Marble Maze задает флаг в обратном вызове OnCriticalError , а затем обрабатывает ошибку во время обычной фазы обработки звука. Дополнительные сведения о обратных вызовах XAudio2 см. в статье XAudio2 Callbacks.

Заключение

Это завернуть образец игры Marble Maze! Хотя это относительно простая игра, она содержит множество важных частей, которые попадают в любую игру UWP DirectX, и это хороший пример, чтобы следовать при создании собственной игры.

Теперь, когда вы закончили работу, попробуйте обработать исходный код и увидеть, что происходит. Или посмотрите , как создать простую игру UWP с помощью DirectX, другой пример игры UWP DirectX.

Готовы идти дальше с DirectX? Затем ознакомьтесь с нашими руководствами по программированию DirectX.

Если вы заинтересованы в разработке игр в UWP в целом, ознакомьтесь с документацией по программированию игры.