Adicionar áudio ao exemplo do Marble Maze
Este documento descreve as principais práticas a serem consideradas ao trabalhar com áudio e mostra como o Marble Maze aplica essas práticas. O Marble Maze usa o Microsoft Media Foundation para carregar recursos de áudio de arquivos e o XAudio2 para misturar e reproduzir áudio e aplicar efeitos ao áudio.
O Marble Maze toca música de fundo e também usa sons de jogo para indicar eventos do jogo, como quando a bola de gude atinge uma parede. Uma parte importante da implementação é que o Marble Maze usa um efeito de reverberação, ou eco, para simular o som de uma bola de gude quando ela salta. A implementação do efeito de reverberação faz com que os ecos cheguem até você de forma mais rápida e alta em salas pequenas; Os ecos são mais silenciosos e chegam até você mais lentamente em salas maiores.
Observação
A amostra de código que corresponde a este documento pode ser encontrada na amostra do jogo Marble Maze no DirectX .
Aqui estão alguns dos principais pontos que este documento discute para quando você trabalha com áudio em seu jogo:
Considere usar o Media Foundation para decodificar ativos de áudio e o XAudio2 para reproduzir áudio. No entanto, se você tiver um mecanismo de carregamento de ativos de áudio existente que funcione em um aplicativo UWP (Plataforma Universal do Windows), poderá usá-lo.
Um gráfico de áudio contém uma voz de origem para cada som ativo, zero ou mais vozes de submix e uma voz de masterização. As vozes de origem podem alimentar vozes de submix e/ou a voz de masterização. As vozes de submix alimentam outras vozes de submix ou a voz de masterização.
Se os arquivos de música de fundo forem grandes, considere transmitir sua música em buffers menores para que menos memória seja usada.
Se fizer sentido fazer isso, pause a reprodução de áudio quando o aplicativo perder o foco ou a visibilidade ou for suspenso. Retome a reprodução quando o aplicativo recuperar o foco, ficar visível ou for retomado.
Defina categorias de áudio para refletir a função de cada som. Por exemplo, você normalmente usa AudioCategory_GameMedia para áudio de fundo do jogo e AudioCategory_GameEffects para efeitos sonoros.
Lide com alterações de dispositivos, incluindo fones de ouvido, liberando e recriando todos os recursos e interfaces de áudio.
Considere se deve compactar arquivos de áudio ao minimizar o espaço em disco e os custos de streaming é um requisito. Caso contrário, você pode deixar o áudio descompactado para que ele carregue mais rápido.
Apresentando o XAudio2 e o Microsoft Media Foundation
O XAudio2 é uma biblioteca de áudio de baixo nível para Windows que dá suporte específico ao áudio do jogo. Ele fornece um mecanismo de processamento de sinal digital (DSP) e áudio gráfico para jogos. O XAudio2 expande seus antecessores, DirectSound e XAudio, dando suporte a tendências de computação, como arquiteturas de ponto flutuante SIMD e áudio HD. Ele também suporta as demandas de processamento de som mais complexas dos jogos de hoje.
O documento Conceitos principais do XAudio2 explica os principais conceitos para usar o XAudio2. Em resumo, os conceitos são:
A interface IXAudio2 é o núcleo do mecanismo XAudio2. O Marble Maze usa essa interface para criar vozes e receber notificações quando o dispositivo de saída é alterado ou falha.
Uma voz processa, ajusta e reproduz dados de áudio.
Uma voz de origem é uma coleção de canais de áudio (mono, 5.1 e assim por diante) e representa um fluxo de dados de áudio. No XAudio2, uma voz de origem é onde o processamento de áudio começa. Normalmente, os dados de som são carregados de uma fonte externa, como um arquivo ou uma rede, e são enviados para uma voz de origem. O Marble Maze usa o Media Foundation para carregar dados de som de arquivos. A Media Foundation é apresentada posteriormente neste documento.
Uma voz de submix processa dados de áudio. Esse processamento pode incluir a alteração do fluxo de áudio ou a combinação de vários fluxos em um. O Marble Maze usa submixes para criar o efeito de reverberação.
Uma voz de masterização combina dados de vozes de origem e submix e envia esses dados para o hardware de áudio.
Um gráfico de áudio contém uma voz de origem para cada som ativo, zero ou mais vozes de submix e apenas uma voz de masterização.
Um retorno de chamada informa ao código do cliente que algum evento ocorreu em uma voz ou em um objeto de mecanismo. Usando retornos de chamada, você pode reutilizar a memória quando o XAudio2 terminar com um buffer, reagir quando o dispositivo de áudio for alterado (por exemplo, quando você conectar ou desconectar fones de ouvido) e muito mais. Lidar com fones de ouvido e alterações de dispositivo mais adiante neste documento explica como o Marble Maze usa esse mecanismo para lidar com alterações de dispositivo.
O Marble Maze usa dois mecanismos de áudio (em outras palavras, dois objetos IXAudio2 ) para processar áudio. Um mecanismo processa a música de fundo e o outro processa os sons do jogo.
O Marble Maze também deve criar uma voz de masterização para cada motor. Lembre-se de que um mecanismo de masterização combina fluxos de áudio em um fluxo e envia esse fluxo para o hardware de áudio. O fluxo de música de fundo, uma voz de origem, gera dados para uma voz de masterização e para duas vozes de submixagem. As vozes de submix executam o efeito de reverberação.
O Media Foundation é uma biblioteca multimídia que suporta muitos formatos de áudio e vídeo. O XAudio2 e o Media Foundation se complementam. O Marble Maze usa o Media Foundation para carregar ativos de áudio de arquivos e usa o XAudio2 para reproduzir áudio. Você não precisa usar o Media Foundation para carregar ativos de áudio. Se você tiver um mecanismo de carregamento de ativos de áudio existente que funcione em aplicativos UWP (Plataforma Universal do Windows), use-o. Áudio, vídeo e câmera discute várias maneiras de implementar áudio em um aplicativo UWP.
Para obter mais informações sobre o XAudio2, consulte o Guia de Programação. Para obter mais informações sobre o Media Foundation, consulte Microsoft Media Foundation.
Inicializando recursos de áudio
O Marble Mazes usa um arquivo Windows Media Audio (.wma) para a música de fundo e arquivos WAV (.wav) para sons de jogo. Esses formatos são suportados pela Media Foundation. Embora o formato de arquivo .wav seja suportado nativamente pelo XAudio2, um jogo precisa analisar o formato de arquivo manualmente para preencher as estruturas de dados XAudio2 apropriadas. O Marble Maze usa o Media Foundation para trabalhar mais facilmente com arquivos .wav. Para obter a lista completa dos formatos de mídia compatíveis com o Media Foundation, consulte Formatos de mídia com suporte no Media Foundation. O Marble Maze não usa formatos de áudio separados em tempo de design e tempo de execução e não usa suporte à compactação XAudio2 ADPCM. Para obter mais informações sobre a compactação ADPCM no XAudio2, consulte Visão geral do ADPCM.
O método Audio::CreateResources , que é chamado de MarbleMazeMain::LoadDeferredResources, carrega os fluxos de áudio dos arquivos, inicializa os objetos de mecanismo XAudio2 e cria as vozes de origem, submix e masterização.
Criando os mecanismos XAudio2
Lembre-se de que o Marble Maze cria um objeto IXAudio2 para representar cada mecanismo de áudio usado. Para criar um mecanismo de áudio, chame o método XAudio2Create . O exemplo a seguir mostra como o Marble Maze cria o mecanismo de áudio que processa a música de fundo.
// In Audio.h
class Audio
{
private:
IXAudio2* m_musicEngine;
// ...
}
// In Audio.cpp
void Audio::CreateResources()
{
try
{
// ...
DX::ThrowIfFailed(
XAudio2Create(&m_musicEngine)
);
// ...
}
// ...
}
O Marble Maze executa uma etapa semelhante para criar o mecanismo de áudio que reproduz sons de jogo.
Como trabalhar com a interface IXAudio2 em um aplicativo UWP difere de um aplicativo da área de trabalho de duas maneiras. Primeiro, você não precisa chamar CoInitializeEx antes de chamar XAudio2Create. Além disso, o IXAudio2 não dá mais suporte à enumeração de dispositivos. Para obter informações sobre como enumerar dispositivos de áudio, consulte Enumerando dispositivos.
Criando as vozes de masterização
O exemplo a seguir mostra como o método Audio::CreateResources cria a voz de masterização para a música de fundo usando o método IXAudio2::CreateMasteringVoice . Neste exemplo, m_musicMasteringVoice é um objeto IXAudio2MasteringVoice. Especificamos dois canais de entrada; Isso simplifica a lógica do efeito de reverberação.
Especificamos 48000 como a taxa de amostragem de entrada. Escolhemos essa taxa de amostragem porque ela representava um equilíbrio entre a qualidade do áudio e a quantidade de processamento de CPU necessária. Uma taxa de amostragem maior exigiria mais processamento da CPU sem ter um benefício de qualidade perceptível.
Por fim, especificamos AudioCategory_GameMedia como a categoria de fluxo de áudio para que os usuários possam ouvir música de um aplicativo diferente enquanto jogam. Quando um aplicativo de música está sendo reproduzido, o Windows silencia todas as vozes criadas pela opção AudioCategory_GameMedia . O usuário ainda ouve sons de jogo porque eles são criados pela opção AudioCategory_GameEffects . Para obter mais informações sobre categorias de áudio, consulte 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
)
);
O método Audio::CreateResources executa uma etapa semelhante para criar a voz de masterização para os sons do jogo, exceto que ele especifica AudioCategory_GameEffects para o parâmetro StreamCategory , que é o padrão.
Criando o efeito de reverberação
Para cada voz, você pode usar o XAudio2 para criar sequências de efeitos que processam áudio. Essa sequência é conhecida como cadeia de efeitos. Use cadeias de efeitos quando quiser aplicar um ou mais efeitos a uma voz. As cadeias de efeitos podem ser destrutivas; ou seja, cada efeito na cadeia pode substituir o buffer de áudio. Essa propriedade é importante porque o XAudio2 não garante que os buffers de saída sejam inicializados com silêncio. Os objetos de efeito são representados no XAudio2 por objetos de processamento de áudio multiplataforma (XAPO). Para obter mais informações sobre o XAPO, consulte Visão geral do XAPO.
Ao criar uma cadeia de efeitos, siga estas etapas:
Crie o objeto de efeito.
Preencha uma estrutura XAUDIO2_EFFECT_DESCRIPTOR com dados de efeito.
Preencha uma estrutura XAUDIO2_EFFECT_CHAIN com dados.
Aplique a cadeia de efeitos a uma voz.
Preencha uma estrutura de parâmetro de efeito e aplique-a ao efeito.
Desative ou ative o efeito sempre que apropriado.
A classe Audio define o método CreateReverb para criar a cadeia de efeitos que implementa a reverberação. Esse método chama o método XAudio2CreateReverb para criar um objeto ComPtr<IUnknown> , soundEffectXAPO, que atua como a voz de submix para o efeito de reverberação.
Microsoft::WRL::ComPtr<IUnknown> soundEffectXAPO;
DX::ThrowIfFailed(
XAudio2CreateReverb(&soundEffectXAPO)
);
A estrutura XAUDIO2_EFFECT_DESCRIPTOR contém informações sobre um XAPO para uso em uma cadeia de efeitos, por exemplo, o número de destino dos canais de saída. O método Audio::CreateReverb cria um objeto XAUDIO2_EFFECT_DESCRIPTOR , soundEffectdescriptor, que é definido como o estado desabilitado, usa dois canais de saída e faz referência a soundEffectXAPO para o efeito de reverberação. soundEffecté iniciado no estado desativado porque o jogo deve definir parâmetros antes que o efeito comece a modificar os sons do jogo. O Marble Maze usa dois canais de saída para simplificar a lógica do efeito de reverberação.
soundEffectdescriptor.InitialState = false;
soundEffectdescriptor.OutputChannels = 2;
soundEffectdescriptor.pEffect = soundEffectXAPO.Get();
Se sua cadeia de efeitos tiver vários efeitos, cada efeito exigirá um objeto. A estrutura XAUDIO2_EFFECT_CHAIN contém a matriz de objetos XAUDIO2_EFFECT_DESCRIPTOR que participam do efeito. O exemplo a seguir mostra como o método Audio::CreateReverb especifica o efeito para implementar a reverberação.
XAUDIO2_EFFECT_CHAIN soundEffectChain;
// ...
soundEffectChain.EffectCount = 1;
soundEffectChain.pEffectDescriptors = &soundEffectdescriptor;
O método Audio::CreateReverb chama o método IXAudio2::CreateSubmixVoice para criar a voz de submix para o efeito. Ele especifica o objeto XAUDIO2_EFFECT_CHAIN, soundEffectChain, para que o parâmetro pEffectChain associe a cadeia de efeitos à voz. O Marble Maze também especifica dois canais de saída e uma taxa de amostragem de 48 kilohertz.
DX::ThrowIfFailed(
engine->CreateSubmixVoice(newSubmix, 2, 48000, 0, 0, nullptr, &soundEffectChain)
);
Dica
Se você quiser anexar uma cadeia de efeitos existente a uma voz de submix existente ou substituir a cadeia de efeitos atual, use o método IXAudio2Voice::SetEffectChain .
O método Audio::CreateReverb chama IXAudio2Voice::SetEffectParameters para definir parâmetros adicionais associados ao efeito. Esse método usa uma estrutura de parâmetro específica para o efeito. Um objeto XAUDIO2FX_REVERB_PARAMETERS , m_reverbParametersSmall, que contém os parâmetros de efeito para reverberação, é inicializado no método Audio::Initialize porque cada efeito de reverberação compartilha os mesmos parâmetros. O exemplo a seguir mostra como o método Audio::Initialize inicializa os parâmetros de reverberação para reverberação de campo próximo.
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;
Este exemplo usa os valores padrão para a maioria dos parâmetros de reverberação, mas define DisableLateField como TRUE para especificar a reverberação de campo próximo, EarlyDiffusion como 4 para simular superfícies planas próximas e LateDiffusion como 15 para simular superfícies distantes muito difusas. Superfícies planas próximas fazem com que os ecos cheguem até você mais rapidamente e alto; superfícies distantes difusas fazem com que os ecos sejam mais silenciosos e cheguem até você mais lentamente. Você pode experimentar valores de reverberação para obter o efeito desejado em seu jogo ou usar o método ReverbConvertI3DL2ToNative para usar parâmetros I3DL2 (Interactive 3D Audio Rendering Guidelines Level 2.0) padrão do setor.
O exemplo a seguir mostra como Audio::CreateReverb define os parâmetros de reverberação. newSubmix é um objeto IXAudio2SubmixVoice**. parameters é um objeto XAUDIO2FX_REVERB_PARAMETERS*.
DX::ThrowIfFailed(
(*newSubmix)->SetEffectParameters(0, parameters, sizeof(m_reverbParametersSmall))
);
O método Audio::CreateReverb é concluído habilitando o efeito usando IXAudio2Voice::EnableEffect se o sinalizador enableEffect estiver definido. Ele também define seu volume usando IXAudio2Voice::SetVolume e matriz de saída usando IXAudio2Voice::SetOutputMatrix. Esta parte define o volume como completo (1.0) e, em seguida, especifica a matriz de volume a ser silenciada para as entradas esquerda e direita e os alto-falantes de saída esquerdo e direito. Fazemos isso porque outro código posteriormente faz cross-fad entre os dois reverbs (simulando a transição de estar perto de uma parede para estar em uma sala grande) ou silencia ambos os reverbs, se necessário. Quando o caminho de reverberação é ativado posteriormente, o jogo define uma matriz de {1.0f, 0.0f, 0.0f, 1.0f} para rotear a saída de reverberação esquerda para a entrada esquerda da voz de masterização e a saída de reverberação direita para a entrada direita da voz de masterização.
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)
);
O Marble Maze chama o método Audio::CreateReverb quatro vezes: duas vezes para a música de fundo e duas vezes para os sons do jogo. O seguinte mostra como o Marble Maze chama o método CreateReverb para a música de fundo.
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersSmall,
&m_musicReverbVoiceSmallRoom,
true
);
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersLarge,
&m_musicReverbVoiceLargeRoom,
true
);
Para obter uma lista de possíveis fontes de efeitos para uso com o XAudio2, consulte Efeitos de áudio do XAudio2.
Carregando dados de áudio do arquivo
O Marble Maze define a classe MediaStreamer, que usa o Media Foundation para carregar recursos de áudio de arquivos. O Marble Maze usa um objeto MediaStreamer para carregar cada arquivo de áudio.
O Marble Maze chama o método MediaStreamer::Initialize para inicializar cada fluxo de áudio. Veja como o método Audio::CreateResources chama MediaStreamer::Initialize para inicializar o fluxo de áudio para a música de fundo:
// 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");
O método MediaStreamer::Initialize começa chamando o método MFStartup para inicializar o Media Foundation. MF_VERSION é uma macro definida em mfapi.h e é o que especificamos como a versão do Media Foundation a ser usada.
DX::ThrowIfFailed(
MFStartup(MF_VERSION)
);
MediaStreamer::Initialize chama MFCreateSourceReaderFromURL para criar um objeto IMFSourceReader . Um objeto IMFSourceReader , m_reader, lê dados de mídia do arquivo especificado por url.
DX::ThrowIfFailed(
MFCreateSourceReaderFromURL(url, nullptr, &m_reader)
);
Em seguida, o método MediaStreamer::Initialize cria um objeto IMFMediaType usando MFCreateMediaType para descrever o formato do fluxo de áudio. Um formato de áudio tem dois tipos: um tipo principal e um subtipo. O tipo principal define o formato geral da mídia, como vídeo, áudio, script e assim por diante. O subtipo define o formato, como PCM, ADPCM ou WMA.
O método MediaStreamer::Initialize usa o método IMFAttributes::SetGUID para especificar o tipo principal (MF_MT_MAJOR_TYPE) como áudio (MFMediaType_Audio) e o tipo secundário (MF_MT_SUBTYPE) como áudio PCM não compactado (MFAudioFormat_PCM). MF_MT_MAJOR_TYPE e MF_MT_SUBTYPE são atributos do Media Foundation. MFMediaType_Audio e MFAudioFormat_PCM são GUIDs de tipo e subtipo; consulte Tipos de mídia de áudio para obter mais informações. O método IMFSourceReader::SetCurrentMediaType associa o tipo de mídia ao leitor de fluxo.
// 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())
);
Em seguida, o método MediaStreamer::Initialize obtém o formato de mídia de saída completo do Media Foundation usando IMFSourceReader::GetCurrentMediaType e chama o método MFCreateWaveFormatExFromMFMediaType para converter o tipo de mídia de áudio do Media Foundation em uma estrutura WAVEFORMATEX . A estrutura WAVEFORMATEX define o formato dos dados de áudio da forma de onda. O Marble Maze usa essa estrutura para criar as vozes de origem e aplicar o filtro passa-baixo ao som de rolamento de bolinhas de gude.
// 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);
Importante
O método MFCreateWaveFormatExFromMFMediaType usa CoTaskMemAlloc para alocar o objeto WAVEFORMATEX. Portanto, certifique-se de chamar CoTaskMemFree quando terminar de usar esse objeto.
O método MediaStreamer::Initialize termina calculando o comprimento do fluxo, m_maxStreamLengthInBytes, em bytes. Para fazer isso, ele chama o método IMFSourceReader::GetPresentationAttribute para obter a duração do fluxo de áudio em unidades de 100 nanossegundos, converte a duração em seções e multiplica pela taxa média de transferência de dados em bytes por segundo. Marble Maze posteriormente usa esse valor para alocar o buffer que contém cada som de jogo.
// 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
);
Criando as vozes de origem
O Marble Maze cria vozes de origem XAudio2 para reproduzir cada um de seus sons e músicas de jogos em vozes de origem. A classe Audio define um objeto IXAudio2SourceVoice para a música de fundo e uma matriz de objetos SoundEffectData para manter os sons do jogo. A estrutura SoundEffectData contém o objeto IXAudio2SourceVoice para um efeito e também define outros dados relacionados ao efeito, como o buffer de áudio. Audio.h define a enumeração SoundEvent . O Marble Maze usa essa enumeração para identificar cada som de jogo. A classe Audio também usa essa enumeração para indexar a matriz de objetos SoundEffectData .
enum SoundEvent
{
RollingEvent = 0,
FallingEvent = 1,
CollisionEvent = 2,
CheckpointEvent = 3,
MenuChangeEvent = 4,
MenuSelectedEvent = 5,
LastSoundEvent,
};
A tabela a seguir mostra a relação entre cada um desses valores, o arquivo que contém os dados de som associados e uma breve descrição do que cada som representa. Os arquivos de áudio estão localizados na pasta \Media\Audio .
Valor de SoundEvent | Nome do arquivo | Descrição |
---|---|---|
Evento Contínuo | MarbleRoll.wav | Jogado enquanto a bola de gude rola. |
Evento de queda | MarbleFall.wav | Jogado quando a bola de gude cai do labirinto. |
Evento de colisão | MarbleHit.wav | Jogado quando a bola de gude colide com o labirinto. |
Evento de ponto de verificação | Checkpoint.wav | Jogado quando a bola de gude passa por um posto de controle. |
MenuChangeEvent | MenuChange.wav | Reproduzido quando o usuário altera o item de menu atual. |
MenuSelectedEvent | MenuSelect.wav | Reproduzido quando o usuário seleciona um item de menu. |
O exemplo a seguir mostra como o método Audio::CreateResources cria a voz de origem para a música de fundo. A estrutura XAUDIO2_SEND_DESCRIPTOR define a voz de destino de destino de outra voz e especifica se um filtro deve ser usado. O Marble Maze chama o método Audio::SetSoundEffectFilter para usar os filtros para alterar o som da bola enquanto ela rola. A estrutura XAUDIO2_VOICE_SENDS define o conjunto de vozes para receber dados de uma única voz de saída. O Marble Maze envia dados da voz de origem para a voz de masterização (para a parte seca ou inalterada de um som reproduzido) e para as duas vozes de submix que implementam a parte úmida ou reverberante de um som reproduzido.
O método IXAudio2::CreateSourceVoice cria e configura uma voz de origem. Ele usa uma estrutura WAVEFORMATEX que define o formato dos buffers de áudio que são enviados para a voz. Como mencionado anteriormente, o Marble Maze usa o formato 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)
);
Tocando música de fundo
Uma voz de origem é criada no estado parado. Marble Maze inicia a música de fundo no loop do jogo. A primeira chamada para MarbleMazeMain::Update chama Audio::Start para iniciar a música de fundo.
if (!m_audio.m_isAudioStarted)
{
m_audio.Start();
}
O método Audio::Start chama IXAudio2SourceVoice::Start para começar a processar a voz de origem para a música de fundo.
void Audio::Start()
{
if (m_engineExperiencedCriticalError)
{
return;
}
HRESULT hr = m_musicSourceVoice->Start(0);
if SUCCEEDED(hr) {
m_isAudioStarted = true;
}
else
{
m_engineExperiencedCriticalError = true;
}
}
A voz de origem passa esses dados de áudio para o próximo estágio do gráfico de áudio. No caso do Marble Maze, o próximo estágio contém duas vozes de submix que aplicam os dois efeitos de reverberação ao áudio. Uma voz de submix aplica uma reverberação de campo tardio próxima; o segundo aplica uma reverberação de campo muito tardio.
A quantidade que cada voz de submix contribui para a mixagem final é determinada pelo tamanho e formato da sala. A reverberação de campo próximo contribui mais quando a bola está perto de uma parede ou em uma sala pequena, e a reverberação de campo tardio contribui mais quando a bola está em um grande espaço. Essa técnica produz um efeito de eco mais realista à medida que a bola de gude se move pelo labirinto. Para saber mais sobre como o Marble Maze implementa esse efeito, consulte Audio::SetRoomSize e Physics::CalculateCurrentRoomSize no código-fonte do Marble Maze.
Observação
Em um jogo em que a maioria dos tamanhos de sala são relativamente os mesmos, você pode usar um modelo de reverberação mais básico. Por exemplo, você pode usar uma configuração de reverberação para todas as salas ou pode criar uma configuração de reverberação predefinida para cada sala.
O método Audio::CreateResources usa o Media Foundation para carregar a música de fundo. Neste ponto, no entanto, a voz de origem não tem dados de áudio para trabalhar. Além disso, como a música de fundo é repetida, a voz de origem deve ser atualizada regularmente com dados para que a música continue a tocar.
Para manter a voz de origem preenchida com dados, o loop do jogo atualiza os buffers de áudio a cada quadro. O método MarbleMazeMain::Render chama Audio::Render para processar o buffer de áudio da música em segundo plano. A classe Audio define uma matriz de três buffers de áudio, m_audioBuffers. Cada buffer contém 64 KB (65536 bytes) de dados. O loop lê dados do objeto Media Foundation e grava esses dados na voz de origem até que a voz de origem tenha três buffers enfileirados.
Cuidado
Embora o Marble Maze use um buffer de 64 KB para armazenar dados de música, talvez seja necessário usar um buffer maior ou menor. Esse valor depende dos requisitos do seu jogo.
// 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;
}
}
O loop também lida quando o objeto Media Foundation atinge o final do fluxo. Nesse caso, ele chama o método IMFSourceReader::SetCurrentPosition para redefinir a posição da fonte de áudio.
void MediaStreamer::Restart()
{
if (m_reader == nullptr)
{
return;
}
PROPVARIANT var = {0};
var.vt = VT_I8;
DX::ThrowIfFailed(
m_reader->SetCurrentPosition(GUID_NULL, var)
);
}
Para implementar o loop de áudio para um único buffer (ou para um som inteiro totalmente carregado na memória), você pode definir o campo XAUDIO2_BUFFER::LoopCount como XAUDIO2_LOOP_INFINITE ao inicializar o som. O Marble Maze usa essa técnica para reproduzir o som de rolamento da bolinha de gude.
if (sound == RollingEvent)
{
m_soundEffects[sound].m_audioBuffer.LoopCount = XAUDIO2_LOOP_INFINITE;
}
No entanto, para a música de fundo, o Marble Maze gerencia os buffers diretamente para que possa controlar melhor a quantidade de memória usada. Quando seus arquivos de música são grandes, você pode transmitir os dados de música para buffers menores. Isso pode ajudar a equilibrar o tamanho da memória com a frequência da capacidade do jogo de processar e transmitir dados de áudio.
Dica
Se o jogo tiver uma taxa de quadros baixa ou variável, o processamento de áudio no thread principal poderá produzir pausas ou estalos inesperados no áudio porque o mecanismo de áudio não tem dados de áudio em buffer suficientes para trabalhar. Se o jogo for sensível a esse problema, considere processar o áudio em um thread separado que não execute a renderização. Essa abordagem é especialmente útil em computadores com vários processadores porque seu jogo pode usar processadores ociosos.
Reagindo a eventos do jogo
A classe Audio fornece métodos como PlaySoundEffect, IsSoundEffectStarted, StopSoundEffect, SetSoundEffectVolume, SetSoundEffectPitch e SetSoundEffectFilter para permitir que o jogo controle quando os sons são reproduzidos e parados e para controlar as propriedades do som, como volume e tom. Por exemplo, se a bola de gude cair do labirinto, MarbleMazeMain::Update chamará o método Audio::P laySoundEffect para reproduzir o som FallingEvent .
m_audio.PlaySoundEffect(FallingEvent);
O método Audio::P laySoundEffect chama o método IXAudio2SourceVoice::Start para iniciar a reprodução do som. Se o método IXAudio2SourceVoice::Start já tiver sido chamado, ele não será iniciado novamente. Audio::P laySoundEffect executa lógica personalizada para determinados sons.
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;
}
Para sons diferentes de rolagem, o método Audio::P laySoundEffect chama IXAudio2SourceVoice::GetState para determinar o número de buffers que a voz de origem está reproduzindo. Ele chama IXAudio2SourceVoice::SubmitSourceBuffer para adicionar os dados de áudio do som à fila de entrada da voz se nenhum buffer estiver ativo. O método Audio::P laySoundEffect também permite que o som de colisão seja reproduzido duas vezes em sequência. Isso ocorre, por exemplo, quando a bola de gude colide com um canto do labirinto.
Conforme já descrito, a classe Audio usa o sinalizador XAUDIO2_LOOP_INFINITE quando inicializa o som para o evento de interrupção. O som inicia a reprodução em loop na primeira vez que Audio::P laySoundEffect é chamado para esse evento. Para simplificar a lógica de reprodução do som de rolamento, o Marble Maze silencia o som em vez de pará-lo. À medida que a bola de gude muda de velocidade, o Marble Maze muda o tom e o volume do som para dar um efeito mais realista. O seguinte mostra como o método MarbleMazeMain::Update atualiza o tom e o volume da bola de gude à medida que sua velocidade muda e como ele silencia o som definindo seu volume como zero quando a bola de gude para.
// 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);
}
Reagindo para suspender e retomar eventos
A estrutura do aplicativo Marble Maze descreve como o Marble Maze oferece suporte à suspensão e à retomada. Quando o jogo é suspenso, o jogo pausa o áudio. Quando o jogo é retomado, o jogo retoma o áudio de onde parou. Fazemos isso para seguir a prática recomendada de não usar recursos quando você sabe que eles não são necessários.
O método Audio::SuspendAudio é chamado quando o jogo é suspenso. Esse método chama o método IXAudio2::StopEngine para interromper todo o áudio. Embora IXAudio2::StopEngine pare toda a saída de áudio imediatamente, ele preserva o gráfico de áudio e seus parâmetros de efeito (por exemplo, o efeito de reverberação aplicado quando a bola de gude salta).
// 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;
}
O método Audio::ResumeAudio é chamado quando o jogo é retomado. Esse método usa o método IXAudio2::StartEngine para reiniciar o áudio. Como a chamada para IXAudio2::StopEngine preserva o grafo de áudio e seus parâmetros de efeito, a saída de áudio é retomada de onde parou.
// 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;
}
}
Lidar com fones de ouvido e alterações de dispositivo
O Marble Maze usa retornos de chamada do mecanismo para lidar com falhas do mecanismo XAudio2, como quando o dispositivo de áudio é alterado. Uma causa provável de uma mudança de dispositivo é quando o usuário do jogo conecta ou desconecta os fones de ouvido. Recomendamos que você implemente o retorno de chamada do mecanismo que lida com as alterações do dispositivo. Caso contrário, o jogo deixará de reproduzir o som quando o usuário conectar ou remover fones de ouvido, até que o jogo seja reiniciado.
Audio.h define a classe AudioEngineCallbacks . Essa classe implementa a interface 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);
};
A interface IXAudio2EngineCallback permite que seu código seja notificado quando ocorrem eventos de processamento de áudio e quando o mecanismo encontra um erro crítico. Para se registrar para retornos de chamada, o Marble Maze chama o método IXAudio2::RegisterForCallbacks em Audio::CreateResources, depois de criar o objeto IXAudio2 para o mecanismo de música.
m_musicEngineCallback.Initialize(this);
m_musicEngine->RegisterForCallbacks(&m_musicEngineCallback);
O Marble Maze não requer notificação quando o processamento de áudio começa ou termina. Portanto, ele implementa os métodos IXAudio2EngineCallback::OnProcessingPassStart e IXAudio2EngineCallback::OnProcessingPassEnd para não fazer nada. Para o método IXAudio2EngineCallback::OnCriticalError , o Marble Maze chama o método SetEngineExperiencedCriticalError , que define o sinalizador 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;
}
Quando ocorre um erro crítico, o processamento de áudio é interrompido e todas as chamadas adicionais para XAudio2 falham. Para se recuperar dessa situação, você deve liberar a instância XAudio2 e criar uma nova. O método Audio::Render , que é chamado do loop do jogo a cada quadro, primeiro verifica o sinalizador m_engineExperiencedCriticalError . Se esse sinalizador estiver definido, ele limpará o sinalizador, liberará a instância XAudio2 atual, inicializará os recursos e iniciará a música em segundo plano.
if (m_engineExperiencedCriticalError)
{
m_engineExperiencedCriticalError = false;
ReleaseResources();
Initialize();
CreateResources();
Start();
if (m_engineExperiencedCriticalError)
{
return;
}
}
O Marble Maze também usa o sinalizador m_engineExperiencedCriticalError para proteger contra chamadas no XAudio2 quando nenhum dispositivo de áudio está disponível. Por exemplo, o método MarbleMazeMain::Update não processa áudio para eventos de interrupção ou colisão quando esse sinalizador é definido. O aplicativo tenta reparar o mecanismo de áudio a cada quadro, se necessário; No entanto, o sinalizador m_engineExperiencedCriticalError sempre poderá ser definido se o computador não tiver um dispositivo de áudio ou se os fones de ouvido estiverem desconectados e não houver outro dispositivo de áudio disponível.
Cuidado
Como regra, não execute operações de bloqueio no corpo de um retorno de chamada do mecanismo. Isso pode causar problemas de desempenho. O Marble Maze define um sinalizador no retorno de chamada OnCriticalError e, posteriormente, manipula o erro durante a fase de processamento de áudio regular. Para obter mais informações sobre retornos de chamada XAudio2, consulte Retornos de chamada XAudio2.
Conclusão
Isso encerra a amostra do jogo Marble Maze! Embora seja um jogo relativamente simples, ele contém muitas das partes importantes que entram em qualquer jogo UWP DirectX e é um bom exemplo a seguir ao criar seu próprio jogo.
Agora que você terminou de acompanhar, tente mexer no código-fonte e ver o que acontece. Ou confira Criar um jogo UWP simples com DirectX, outro exemplo de jogo UWP DirectX.
Pronto para ir mais longe com o DirectX? Então confira nossos guias em Programação DirectX.
Se você estiver interessado no desenvolvimento de jogos na UWP em geral, consulte a documentação em Programação de jogos.