Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
En este documento se describen las prácticas clave que se deben tener en cuenta al trabajar con audio y se muestra cómo Marble Maze aplica estas prácticas. Marble Maze usa Microsoft Media Foundation para cargar recursos de audio de archivos y XAudio2 para mezclar y reproducir audio y aplicar efectos al audio.
Marble Maze reproduce música en segundo plano, y también usa sonidos de juego para indicar eventos de juego, como cuando la canica golpea una pared. Una parte importante de la implementación es que Marble Maze usa un efecto reverberador o eco para simular el sonido de una canica cuando rebota. La implementación del efecto reverberador hace que los ecos lleguen más rápido y fuertemente en salas pequeñas; los ecos son más silenciosos y llegan más lentamente en salas más grandes.
Nota:
El código de ejemplo que corresponde a este documento se encuentra en el ejemplo de juego DirectX Marble Maze .
Estos son algunos de los puntos clave que describe este documento para cuando trabajas con audio en tu juego:
Considere la posibilidad de usar Media Foundation para descodificar recursos de audio y XAudio2 para reproducir audio. Sin embargo, si tienes un mecanismo de carga de activos de audio existente que funciona en una aplicación para la Plataforma universal de Windows (UWP), puedes usarlo.
Un gráfico de audio contiene una voz de origen para cada sonido activo, cero o más voces de submezcla y una voz de maestro. Las voces de origen pueden introducirse en voces de submezcla o en la voz de masterización. Las voces de submezcla se introducen en otras voces de submezcla o en la voz maestra.
Si los archivos de música de fondo son grandes, considere la posibilidad de transmitir la música a búferes más pequeños para que se use menos memoria.
Si tiene sentido hacerlo, pause la reproducción de audio cuando la aplicación pierda el foco o la visibilidad, o se suspenda. Reanude la reproducción cuando la aplicación recupere el foco, se vuelva visible o se reanude.
Establezca categorías de audio para reflejar el rol de cada sonido. Por ejemplo, normalmente usas AudioCategory_GameMedia para el audio de fondo del juego y AudioCategory_GameEffects para efectos de sonido.
Controle los cambios del dispositivo, incluidos los auriculares, liberando y recreando todos los recursos de audio e interfaces.
Considere si se deben comprimir archivos de audio cuando minimizar el espacio en disco y los costos de streaming sea un requisito. De lo contrario, puede dejar el audio sin comprimir para que se cargue más rápido.
Presentación de XAudio2 y Microsoft Media Foundation
XAudio2 es una biblioteca de audio de bajo nivel para Windows que admite específicamente audio de juego. Proporciona un procesamiento de señal digital (DSP) y un motor de grafos de audio para juegos. XAudio2 se amplía a partir de sus predecesores, DirectSound y XAudio, al admitir tendencias en computación como arquitecturas de punto flotante SIMD y audio HD. También admite las demandas de procesamiento de sonido más complejas de los juegos actuales.
El documento conceptos clave de XAudio2 explica los conceptos clave para usar XAudio2. En resumen, los conceptos son:
La interfaz IXAudio2 es el núcleo del motor XAudio2. Marble Maze usa esta interfaz para crear voces y recibir notificaciones cuando el dispositivo de salida cambia o produce un error.
Una voz procesa, ajusta y reproduce datos de audio.
Un de voz de origen es una colección de canales de audio (mono, 5.1, etc.) y representa una secuencia de datos de audio. En XAudio2, una voz de origen es donde comienza el procesamiento de audio. Normalmente, los datos de sonido se cargan desde un origen externo, como un archivo o una red, y se envían a una voz de origen. Marble Maze usa Media Foundation para cargar datos de sonido de archivos. Media Foundation se presenta más adelante en este documento.
Una submezcla de voz procesa los datos de audio. Este procesamiento puede incluir cambiar la secuencia de audio o combinar varias secuencias en una. Marble Maze usa submezclas para crear el efecto reverberador.
Un de voz de masterización combina datos de voces de origen y submezcla y envía esos datos al hardware de audio.
Un gráfico de audio contiene una voz fuente para cada sonido activo, cero o más voces de submezcla y solo una voz maestra.
Un callback informa al código cliente que ocurrió un evento en una voz o en un objeto del motor. Al usar funciones de retrollamada, puede reutilizar la memoria cuando XAudio2 termina con un búfer, reaccionar ante cambios en el dispositivo de audio (por ejemplo, al conectar o desconectar auriculares) y más. Control de auriculares y cambios de dispositivo más adelante en este documento explica cómo Marble Maze usa este mecanismo para controlar los cambios del dispositivo.
Marble Maze usa dos motores de audio (es decir, dos IXAudio2 objetos) para procesar el audio. Un motor procesa la música de fondo y el otro motor procesa sonidos de juego.
Marble Maze también debe generar una voz maestra para cada motor. Recuerde que un motor de masterización combina secuencias de audio en una secuencia y envía esa secuencia al hardware de audio. La transmisión de música de fondo, una voz de origen, genera datos en una voz de masterización y en dos voces de submezcla. Las voces de submezcla realizan el efecto de reverberación.
Media Foundation es una biblioteca multimedia que admite muchos formatos de audio y vídeo. XAudio2 y Media Foundation se complementan entre sí. Marble Maze usa Media Foundation para cargar recursos de audio de archivos y usa XAudio2 para reproducir audio. No tiene que usar Media Foundation para cargar recursos de audio. Si tienes un mecanismo de carga de recursos de audio existente que funciona en aplicaciones para la Plataforma universal de Windows (UWP), úsela. audio, vídeo y cámara analiza varias maneras de implementar audio en una aplicación para UWP.
Para obtener más información sobre XAudio2, consulte Guía de programación. Para obtener más información sobre Media Foundation, vea Microsoft Media Foundation.
Inicialización de recursos de audio
Marble Mazes usa un archivo de Audio de Windows Media (.wma) para los archivos de música de fondo y WAV (.wav) para sonidos de juego. Media Foundation admite estos formatos. Aunque el formato de archivo .wav es compatible de forma nativa con XAudio2, un juego tiene que analizar manualmente el formato de archivo para rellenar las estructuras de datos XAudio2 adecuadas. Marble Maze usa Media Foundation para trabajar más fácilmente con archivos .wav. Para obtener la lista completa de los formatos multimedia compatibles con Media Foundation, consulte formatos multimedia admitidos en Media Foundation. Marble Maze no utiliza formatos de audio separados en tiempo de diseño y en tiempo de ejecución, y no son compatibles con soporte de compresión XAudio2 ADPCM. Para obtener más información sobre la compresión de ADPCM en XAudio2, consulte ADPCM Overview.
El método Audio::CreateResources, al que se llama desde MarbleMazeMain::LoadDeferredResources, carga los flujos de audio de los archivos, inicializa los objetos del motor XAudio2 y crea las voces de origen, submezcla y maestro.
Creación de los motores XAudio2
Recuerde que Marble Maze crea un objeto IXAudio2 para representar cada motor de audio que usa. Para crear un motor de audio, llame al método XAudio2Create. En el ejemplo siguiente se muestra cómo Marble Maze crea el motor de audio que procesa la música de fondo.
// In Audio.h
class Audio
{
private:
IXAudio2* m_musicEngine;
// ...
}
// In Audio.cpp
void Audio::CreateResources()
{
try
{
// ...
DX::ThrowIfFailed(
XAudio2Create(&m_musicEngine)
);
// ...
}
// ...
}
Marble Maze realiza un paso similar para crear el motor de audio que reproduce sonidos de juego.
Cómo trabajar con la interfaz de IXAudio2 en una aplicación para UWP difiere de una aplicación de escritorio de dos maneras. En primer lugar, no es necesario llamar a CoInitializeEx antes de llamar a XAudio2Create. Además, ixAudio2 ya no admite la enumeración de dispositivos. Para obtener información sobre cómo enumerar dispositivos de audio, consulte Enumeración de dispositivos.
Creación de voces para masterización
En el ejemplo siguiente se muestra cómo el método Audio::CreateResources crea la voz de maestro para la música de fondo mediante el método IXAudio2::CreateMasteringVoice. En este ejemplo, m_musicMasteringVoice es un objeto IXAudio2MasteringVoice. Especificamos dos canales de entrada; esto simplifica la lógica del efecto de reverberación.
Especificamos 48000 como frecuencia de muestreo de entrada. Hemos elegido esta frecuencia de muestreo porque representa un equilibrio entre la calidad de audio y la cantidad de procesamiento de CPU necesario. Una mayor frecuencia de muestreo habría requerido más procesamiento de CPU sin tener una ventaja notable de calidad.
Por último, especificamos AudioCategory_GameMedia como categoría de secuencia de audio para que los usuarios puedan escuchar música desde una aplicación diferente a medida que juegan el juego. Cuando se reproduce una aplicación de música, Windowsmuta todas las voces creadas por la opción AudioCategory_GameMedia. El usuario sigue escuchando sonidos de juego porque se crean mediante la opción AudioCategory_GameEffects. Para obtener más información sobre las categorías de audio, consulta 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
)
);
El método Audio::CreateResources realiza un paso similar para crear la voz maestra para los sonidos del juego, con la excepción de que especifica AudioCategory_GameEffects para el parámetro StreamCategory, siendo este el valor predeterminado.
Creación del efecto de reverberación
Para cada voz, puede usar XAudio2 para crear secuencias de efectos que procesan el audio. Esta secuencia se conoce como una cadena de efectos. Use cadenas de efectos cuando desee aplicar uno o varios efectos a una voz. Las cadenas de efecto pueden ser destructivas; es decir, cada efecto de la cadena puede sobrescribir el búfer de audio. Esta propiedad es importante porque XAudio2 no garantiza que los búferes de salida se inicialicen con silencio. Los objetos de efecto se representan en XAudio2 mediante objetos de procesamiento de audio multiplataforma (XAPO). Para obtener más información sobre XAPO, consulte información general de XAPO.
Al crear una cadena de efectos, siga estos pasos:
Cree el objeto de efecto.
Rellene una estructura de XAUDIO2_EFFECT_DESCRIPTOR con datos de efecto.
Rellene una estructura de XAUDIO2_EFFECT_CHAIN con datos.
Aplique la cadena de efectos a una voz.
Rellene una estructura de parámetros de efecto y aplíquela al efecto.
Deshabilite o habilite el efecto siempre que corresponda.
La clase Audio define el método CreateReverb para crear la cadena de efectos que implementa la reverberación. Este método llama al método XAudio2CreateReverb
Microsoft::WRL::ComPtr<IUnknown> soundEffectXAPO;
DX::ThrowIfFailed(
XAudio2CreateReverb(&soundEffectXAPO)
);
La estructura XAUDIO2_EFFECT_DESCRIPTOR contiene información sobre un XAPO para su uso en una cadena de efectos, por ejemplo, el número de canales de salida de destino. El método Audio::CreateReverb crea un objeto XAUDIO2_EFFECT_DESCRIPTOR, soundEffectdescriptor, que se establece en el estado deshabilitado, usa dos canales de salida y hace referencia a soundEffectXAPO para el efecto de reverberación. soundEffectdescriptor comienza en estado deshabilitado porque el juego debe establecer parámetros antes de que el efecto empiece a modificar los sonidos del juego. Marble Maze usa dos canales de salida para simplificar la lógica del efecto de reverberación.
soundEffectdescriptor.InitialState = false;
soundEffectdescriptor.OutputChannels = 2;
soundEffectdescriptor.pEffect = soundEffectXAPO.Get();
Si la cadena de efectos tiene varios efectos, cada efecto requiere un objeto . La estructura XAUDIO2_EFFECT_CHAIN contiene la matriz de objetos XAUDIO2_EFFECT_DESCRIPTOR que intervienen en el efecto. En el ejemplo siguiente se muestra cómo el método Audio::CreateReverb especifica el único efecto de aplicar la reverberación.
XAUDIO2_EFFECT_CHAIN soundEffectChain;
// ...
soundEffectChain.EffectCount = 1;
soundEffectChain.pEffectDescriptors = &soundEffectdescriptor;
El método Audio::CreateReverb llama al método IXAudio2::CreateSubmixVoice para crear la voz de mezcla secundaria para el efecto. Especifica el objeto XAUDIO2_EFFECT_CHAIN, soundEffectChain, para el parámetro pEffectChain, a fin de asociar la cadena de efectos con la voz. Marble Maze también especifica dos canales de salida y una frecuencia de muestreo de 48 kilohercios.
DX::ThrowIfFailed(
engine->CreateSubmixVoice(newSubmix, 2, 48000, 0, 0, nullptr, &soundEffectChain)
);
Sugerencia
Si desea adjuntar una cadena de efectos existente a una voz de submezcla existente o desea reemplazar la cadena de efectos actual, use el método IXAudio2Voice::SetEffectChain.
El método Audio::CreateReverb llama a IXAudio2Voice::SetEffectParameters para establecer parámetros adicionales asociados al efecto. Este método toma una estructura de parámetros específica del efecto. Un objeto XAUDIO2FX_REVERB_PARAMETERS, m_reverbParametersSmall, que contiene los parámetros de efecto para reverberación, se inicializa en el método Audio::Initialize porque cada efecto de reverberación comparte los mismos parámetros. En el ejemplo siguiente se muestra cómo el método Audio::Initialize inicializa los parámetros de reverberación para la reverberación casi de campo.
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;
En este ejemplo se usan los valores predeterminados para la mayoría de los parámetros de reverberación, pero establece DisableLateField en TRUE para especificar la reverberación casi de campo, EarlyDiffusion a 4 para simular superficies planas cercanas y LateDiffusion a 15 para simular superficies distantes muy difusas. Las superficies cercanas planas hacen que los ecos lleguen más rápido y fuertemente; las superficies distantes difusas hacen que los ecos sean más silenciosos y alcancen más lentamente. Puedes experimentar con valores de reverberación para obtener el efecto deseado en tu juego o utilizar el método ReverbConvertI3DL2ToNative para emplear los parámetros estándar I3DL2 (Interactive 3D Audio Rendering Guidelines Level 2.0) del sector.
En el ejemplo siguiente se muestra cómo Audio::CreateReverb establece los parámetros de reverberación. newSubmix es un objeto IXAudio2SubmixVoice**. parámetros es un objeto XAUDIO2FX_REVERB_PARAMETERS* .
DX::ThrowIfFailed(
(*newSubmix)->SetEffectParameters(0, parameters, sizeof(m_reverbParametersSmall))
);
El método Audio::CreateReverb finaliza al habilitar el efecto utilizando IXAudio2Voice::EnableEffect si está activado el indicador enableEffect. También establece su volumen mediante IXAudio2Voice::SetVolume y matriz de salida mediante IXAudio2Voice::SetOutputMatrix. Esta parte establece el volumen en completo (1.0) y, a continuación, especifica la matriz de volumen que se va a silenciar para las entradas izquierda y derecha y los altavoces de salida izquierdo y derecho. Lo hacemos porque otro código más adelante funde gradualmente entre las dos reverberaciones (simulando la transición de estar cerca de una pared a estar en un espacio grande) o silencia ambas reverberaciones si es necesario. Cuando la ruta de reverberación se desmuta más adelante, el juego establece una matriz de {1.0f, 0.0f, 0.0f, 1.0f} para enrutar la salida de reverberación izquierda a la entrada izquierda de la voz de maestro y la salida de reverberación derecha a la entrada derecha de la voz de maestro.
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 llama al Audio::CreateReverb método cuatro veces: dos veces para la música de fondo y dos veces para los sonidos del juego. A continuación se muestra cómo Marble Maze llama al método CreateReverb para la música de fondo.
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersSmall,
&m_musicReverbVoiceSmallRoom,
true
);
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersLarge,
&m_musicReverbVoiceLargeRoom,
true
);
Para obtener una lista de posibles orígenes de efectos para su uso con XAudio2, vea efectos de audio XAudio2.
Carga de datos de audio desde el archivo
Marble Maze define la clase MediaStreamer, que usa Media Foundation para cargar recursos de audio de archivos. Marble Maze usa un objeto MediaStreamer para cargar cada archivo de audio.
Marble Maze llama al método MediaStreamer::Initialize para inicializar cada secuencia de audio. Este es el modo en que el método Audio::CreateResources llama a MediaStreamer::Initialize para inicializar la secuencia de audio para la música de fondo:
// 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");
El método MediaStreamer::Initialize comienza llamando al método MFStartup para inicializar Media Foundation. MF_VERSION es una macro definida en mfapi.hy es lo que especificamos como la versión de Media Foundation que se va a usar.
DX::ThrowIfFailed(
MFStartup(MF_VERSION)
);
MediaStreamer::Initialize llama a MFCreateSourceReaderFromURL para crear un objeto IMFSourceReader. Un objeto
DX::ThrowIfFailed(
MFCreateSourceReaderFromURL(url, nullptr, &m_reader)
);
A continuación, el método MediaStreamer::Initialize crea un objeto IMFMediaType mediante MFCreateMediaType con el fin de describir el formato del flujo de audio. Un formato de audio tiene dos tipos: un tipo principal y un subtipo. El tipo principal define el formato general del medio, como vídeo, audio, script, etc. El subtipo define el formato, como PCM, ADPCM o WMA.
El método MediaStreamer::Initialize usa el método IMFAttributes::SetGUID para especificar el tipo principal (MF_MT_MAJOR_TYPE) como audio (MFMediaType_Audio) y el tipo secundario (MF_MT_SUBTYPE) como audio PCM sin comprimir (MFAudioFormat_PCM). MF_MT_MAJOR_TYPE y MF_MT_SUBTYPE son atributos de Media Foundation. MFMediaType_Audio y MFAudioFormat_PCM son GUID de tipo y subtipo; consulte Tipos de Medios de Audio para obtener más información. El método IMFSourceReader::SetCurrentMediaType asocia el tipo de medio al lector de secuencias.
// 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())
);
A continuación, el método MediaStreamer::Initialize obtiene el formato multimedia de salida completo de Media Foundation mediante IMFSourceReader::GetCurrentMediaType y llama al método MFCreateWaveFormatExFromMFMediaType para convertir el tipo de medio de audio de Media Foundation en una estructura WAVEFORMATEX. La estructura WAVEFORMATEX define el formato de los datos de audio de forma de onda. Marble Maze usa esta estructura para crear las voces de origen y aplicar el filtro de paso bajo al sonido del mármol al rodar.
// 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
El método MFCreateWaveFormatExFromMFMediaType usa CoTaskMemAlloc para asignar el objeto WAVEFORMATEX. Por lo tanto, asegúrese de llamar a CoTaskMemFree cuando haya terminado de usar este objeto.
El método MediaStreamer::Initialize finaliza calculando la longitud de la secuencia, m_maxStreamLengthInBytes, en bytes. Para ello, llama al método IMFSourceReader::GetPresentationAttribute para obtener la duración de la secuencia de audio en unidades de 100 nanosegundos, convierte la duración en secciones y, a continuación, multiplica por la velocidad media de transferencia de datos en bytes por segundo. Marble Maze usa más adelante este valor para asignar el búfer que contiene cada sonido de juego.
// 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
);
Creación de las voces de fuente
Marble Maze crea voces de origen XAudio2 para reproducir cada uno de sus sonidos de juego y música en estas voces de origen. La clase Audio define un objeto IXAudio2SourceVoice para la música de fondo y una matriz de objetos SoundEffectData para contener los sonidos del juego. La estructura SoundEffectData
enum SoundEvent
{
RollingEvent = 0,
FallingEvent = 1,
CollisionEvent = 2,
CheckpointEvent = 3,
MenuChangeEvent = 4,
MenuSelectedEvent = 5,
LastSoundEvent,
};
En la tabla siguiente se muestra la relación entre cada uno de estos valores, el archivo que contiene los datos de sonido asociados y una breve descripción de lo que representa cada sonido. Los archivos de audio se encuentran en la carpeta \Media\Audio.
Valor de SoundEvent | Nombre del archivo | Descripción |
---|---|---|
RollingEvent | MarbleRoll.wav | Tocó como rollos de mármol. |
Evento de Caída | MarbleFall.wav | Se reproduce cuando la canica cae del laberinto. |
Evento de colisión | MarbleHit.wav | Se reproduce cuando la canica colisiona con el laberinto. |
CheckpointEvent | Punto de Control.wav | Se reproduce cuando la canica pasa sobre un punto de control. |
EventoDeCambioDeMenú | MenuChange.wav | Se reproduce cuando el usuario cambia el elemento de menú actual. |
EventoSeleccionadoDelMenú | MenuSelect.wav | Se reproduce cuando el usuario selecciona un elemento de menú. |
En el ejemplo siguiente se muestra cómo el método Audio::CreateResources crea la voz de origen para la música de fondo. La estructura XAUDIO2_SEND_DESCRIPTOR define la voz de destino desde otra voz y especifica si se debe utilizar un filtro. Marble Maze llama al método Audio::SetSoundEffectFilter para utilizar los filtros y cambiar el sonido de la pelota a medida que rueda. La estructura XAUDIO2_VOICE_SENDS define el conjunto de voces para recibir datos de una sola voz de salida. Marble Maze envía datos de la voz de origen a la voz de masterización (para la parte seca, o inalterada, de un sonido de reproducción) y a las dos voces de submezcla que implementan la parte húmeda, o reverberante, de un sonido de reproducción.
El método IXAudio2::CreateSourceVoice crea y configura una voz de origen. Toma una estructura WAVEFORMATEX que determina el formato de los búferes de audio enviados a la voz. Como se mencionó anteriormente, Marble Maze usa el 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)
);
Reproducción de música de fondo
Se crea una voz de origen en estado detenido. Marble Maze inicia la música de fondo en el bucle del juego. La primera invocación de MarbleMazeMain::Update hace una llamada a Audio::Start para iniciar la música de fondo.
if (!m_audio.m_isAudioStarted)
{
m_audio.Start();
}
El método Audio::Start llama a IXAudio2SourceVoice::Start para empezar a procesar la voz de origen de la música de fondo.
void Audio::Start()
{
if (m_engineExperiencedCriticalError)
{
return;
}
HRESULT hr = m_musicSourceVoice->Start(0);
if SUCCEEDED(hr) {
m_isAudioStarted = true;
}
else
{
m_engineExperiencedCriticalError = true;
}
}
La voz de origen pasa esos datos de audio a la siguiente fase del gráfico de audio. En el caso de Marble Maze, la siguiente fase contiene dos voces de submezcla que aplican los dos efectos de reverberación al audio. Una voz de submezcla aplica una reverberación de campo final cerrada; el segundo aplica una reverberación de campo muy tarde.
La cantidad que cada voz de submezcla contribuye a la mezcla final viene determinada por el tamaño y la forma de la sala. La reverberación de campo cercano contribuye más cuando la bola está cerca de una pared o en una habitación pequeña, y la reverberación de campo tardía contribuye más cuando la bola está en un espacio grande. Esta técnica produce un efecto de eco más realista a medida que la canica se mueve a través del laberinto. Para obtener más información sobre cómo Marble Maze implementa este efecto, consulte Audio::SetRoomSize y Physics::CalculateCurrentRoomSize en el código fuente de Marble Maze.
Nota:
En un juego en el que la mayoría de los tamaños de habitación son relativamente iguales, puedes usar un modelo de reverberación más básico. Por ejemplo, puede usar una configuración de reverberación para todas las salas o puede crear una configuración de reverberación predefinida para cada sala.
El método Audio::CreateResources usa Media Foundation para cargar la música de fondo. Sin embargo, en este momento, la voz de origen no tiene datos de audio con los que trabajar. Además, dado que la música de fondo se reproduce en bucle, la voz de origen debe actualizarse periódicamente con datos para que la música continúe reproduciéndose.
Para mantener la voz de origen llena de datos, el bucle del juego actualiza los búferes de audio cada fotograma. El método MarbleMazeMain::Render llama al método Audio::Render para procesar el búfer de audio de música en segundo plano. La clase Audio define una matriz de tres búferes de audio, m_audioBuffers. Cada búfer contiene 64 KB (65536 bytes) de datos. El bucle lee datos del objeto Media Foundation y escribe esos datos en la voz de origen hasta que la voz de origen tiene tres búferes en espera.
Precaución
Aunque Marble Maze usa un búfer de 64 KB para almacenar datos de música, es posible que tenga que usar un búfer más grande o más pequeño. Esta cantidad depende de los requisitos de tu juego.
// 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;
}
}
El bucle también controla cuando el objeto Media Foundation llega al final de la secuencia. En este caso, llama al método IMFSourceReader::SetCurrentPosition para restablecer la posición del origen de audio.
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 el bucle de audio para un único búfer (o para un sonido completo que está totalmente cargado en la memoria), puede establecer el campo XAUDIO2_BUFFER::LoopCount en XAUDIO2_LOOP_INFINITE al inicializar el sonido. Marble Maze utiliza esta técnica para reproducir el sonido rodante de la canica.
if (sound == RollingEvent)
{
m_soundEffects[sound].m_audioBuffer.LoopCount = XAUDIO2_LOOP_INFINITE;
}
Sin embargo, para la música de fondo, Marble Maze administra los búferes directamente para que pueda controlar mejor la cantidad de memoria que se usa. Cuando los archivos de música son grandes, puede transmitir los datos de música a búferes más pequeños. Si lo hace, puede ayudar a equilibrar el tamaño de memoria con la frecuencia de la capacidad del juego para procesar y transmitir datos de audio.
Sugerencia
Si el juego tiene una velocidad de fotogramas baja o variable, el procesamiento de audio en el subproceso principal puede producir pausas inesperadas o chasquidos en el audio porque el motor de audio no tiene suficientes datos de audio buffereados con los que trabajar. Si el juego es sensible a este problema, considere la posibilidad de procesar audio en un subproceso independiente que no realiza la representación. Este enfoque es especialmente útil en equipos que tienen varios procesadores porque el juego puede usar procesadores inactivos.
Reacción a eventos de juego
La clase Audio proporciona métodos como PlaySoundEffect, IsSoundEffectStarted, StopSoundEffect, SetSoundEffectVolume, SetSoundEffectPitchy SetSoundEffectFilter para permitir que el juego controle cuándo se reproducen y detienen los sonidos, y para controlar propiedades sonoras como volumen y tono. Por ejemplo, si la canica cae del laberinto, MarbleMazeMain::Update llama al método Audio::PlaySoundEffect para reproducir el sonido FallingEvent.
m_audio.PlaySoundEffect(FallingEvent);
El método Audio::PlaySoundEffect llama al método IXAudio2SourceVoice::Start para iniciar la reproducción del sonido. Si ya se ha llamado al método IXAudio2SourceVoice::Start, no se volverá a iniciar. Audio::PlaySoundEffect realiza lógica personalizada para determinados sonidos.
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;
}
En el caso de sonidos distintos del rodamiento, el método Audio::PlaySoundEffect llama a IXAudio2SourceVoice::GetState para determinar el número de búferes que está reproduciendo la fuente de voz. Llama a IXAudio2SourceVoice::SubmitSourceBuffer para agregar los datos de audio del sonido a la cola de entrada de la fuente de voz si no hay ningún búfer activo. El método Audio::PlaySoundEffect también permite reproducir el sonido de colisión dos veces en secuencia. Esto sucede cuando, por ejemplo, la canica choca con una esquina del laberinto.
Como ya se ha descrito, la clase Audio usa la marca XAUDIO2_LOOP_INFINITE cuando inicializa el sonido para el evento gradual. El sonido comienza a reproducirse en bucle la primera vez que se llama a Audio::PlaySoundEffect para este evento. Para simplificar la lógica de reproducción para el sonido de rodadura, Marble Maze muta el sonido en lugar de detenerlo. A medida que la canica cambia la velocidad, Marble Maze cambia el tono y el volumen del sonido para darle un efecto más realista. A continuación se muestra cómo el método MarbleMazeMain::Update actualiza el tono y el volumen de la canica a medida que cambia su velocidad y cómo silencia el sonido estableciendo su volumen en cero cuando la canica se detiene.
// 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);
}
Cómo reaccionar ante eventos de suspensión y reanudación
La estructura de la aplicación Marble Maze describe cómo Marble Maze admite las funciones de suspensión y reanudación. Cuando se suspende el juego, el juego pausa el audio. Cuando el juego se reanuda, el juego reanuda el audio donde se dejó. Lo hacemos para seguir el procedimiento recomendado de no usar recursos cuando sepa que no son necesarios.
El método Audio::SuspendAudio se llama cuando el juego se suspende. Este método llama al método IXAudio2::StopEngine para detener todo el audio. Aunque IXAudio2::StopEngine detiene toda la salida de audio inmediatamente, conserva el grafo de audio y sus parámetros de efecto (por ejemplo, el efecto de reverberación que se aplica cuando la canica rebota).
// 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;
}
Se llama al método Audio::ResumeAudio cuando se reanuda el juego. Este método usa el método IXAudio2::StartEngine para reiniciar el audio. Puesto que la llamada a IXAudio2::StopEngine conserva el grafo de audio y sus parámetros de efecto, la salida de audio se reanuda donde se dejó.
// 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;
}
}
Controlar los auriculares y los cambios de dispositivo
Marble Maze utiliza llamadas de retorno del motor para gestionar los fallos del motor XAudio2, como cuando el dispositivo de audio cambia. Una causa probable de un cambio de dispositivo es cuando el usuario del juego se conecta o desconecta los auriculares. Recomendamos que implementes el callback del motor que gestiona los cambios del dispositivo. De lo contrario, el juego dejará de reproducir el sonido cuando el usuario conecte o quite auriculares, hasta que se reinicie el juego.
Audio.h define la clase AudioEngineCallbacks. Esta clase implementa la interfaz 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);
};
La interfaz IXAudio2EngineCallback permite que el código se notifique cuando se produzcan eventos de procesamiento de audio y cuando el motor encuentre un error crítico. Para registrar callbacks, Marble Maze llama al método IXAudio2::RegisterForCallbacks en Audio::CreateResourcesdespués de crear el objeto IXAudio2 del motor de música.
m_musicEngineCallback.Initialize(this);
m_musicEngine->RegisterForCallbacks(&m_musicEngineCallback);
Marble Maze no requiere notificación cuando se inicia o finaliza el procesamiento de audio. Por lo tanto, implementa los métodos IXAudio2EngineCallback::OnProcessingPassStart y IXAudio2EngineCallback::OnProcessingPassEnd para no hacer nada. Para el método IXAudio2EngineCallback::OnCriticalError, Marble Maze llama al método SetEngineExperiencedCriticalError, que establece el indicador 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;
}
Cuando se produce un error crítico, el procesamiento de audio se detiene y se producen errores en todas las llamadas adicionales a XAudio2. Para recuperarse de esta situación, debe liberar la instancia de XAudio2 y crear una nueva. El método Audio::Render, al que se llama desde el bucle de juego en cada fotograma, comprueba primero la bandera m_engineExperiencedCriticalError. Si se establece esta marca, borra la marca, libera la instancia XAudio2 actual, inicializa los recursos y, a continuación, inicia la música de fondo.
if (m_engineExperiencedCriticalError)
{
m_engineExperiencedCriticalError = false;
ReleaseResources();
Initialize();
CreateResources();
Start();
if (m_engineExperiencedCriticalError)
{
return;
}
}
Marble Maze también usa el indicador m_engineExperiencedCriticalError para evitar llamadas a XAudio2 cuando no hay ningún dispositivo de audio disponible. Por ejemplo, el método MarbleMazeMain::Update no procesa el audio para eventos de rodadura o colisión cuando se establece este indicador. La aplicación intenta reparar el motor de audio cada fotograma, si es necesario; sin embargo, el indicador m_engineExperiencedCriticalError podría estar configurado siempre si el equipo no cuenta con un dispositivo de audio o si los auriculares están desconectados y no hay ningún otro dispositivo de audio disponible.
Precaución
Como norma general, no realice operaciones de bloqueo en el cuerpo de una devolución de llamada del motor. Si lo hace, puede causar problemas de rendimiento. Marble Maze establece una bandera en la OnCriticalError devolución de llamada y, posteriormente, gestiona el error durante la fase de procesamiento de audio normal. Para obtener más información sobre los callbacks XAudio2, consulte callbacks XAudio2.
Conclusión
¡Con esto concluimos el ejemplo del juego Marble Maze! Aunque es un juego relativamente sencillo, contiene muchas de las partes importantes que entran en cualquier juego directX de UWP, y es un buen ejemplo para seguir al hacer su propio juego.
Ahora que ha terminado de seguirlo, pruebe a probar con el código fuente y vea lo que sucede. O echa un vistazo Crear un sencillo juego para UWP con DirectX, otro ejemplo de juego directX para UWP.
¿Listo para seguir adelante con DirectX? A continuación, consulte nuestras guías en programación de DirectX.
Si estás interesado en el desarrollo de juegos en UWP en general, consulta la documentación en Programación de juegos.