Compartir a través de


Adición de audio a la muestra de Marble Maze

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 Marble Maze de DirectX.

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 de 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 de maestro.

  • 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 al minimizar el espacio en disco y los costos de streaming es 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 expande en sus predecesores, DirectSound y XAudio, al admitir tendencias informáticas 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.

En el documento XAudio2 Key Concepts se explican 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.

  • Una 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 voz de submezcla 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.

  • Una voz de maestro 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 de origen para cada sonido activo, cero o más voces de submezcla, y solo una voz de maestro.

  • Una devolución de llamada informa al código de cliente de que algún evento se ha producido en una voz o en un objeto de motor. Al usar devoluciones de llamada, puede reutilizar la memoria cuando XAudio2 termina con un búfer, reaccionar cuando cambia el dispositivo de audio (por ejemplo, cuando se conectan o desconectan auriculares), etc. El control de los auriculares y los 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 objetos IXAudio2 ) 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 crear una voz de maestro 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 secuencia 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 de Plataforma universal de Windows (UWP), úsela. Audio, vídeo y cámara describen varias maneras de implementar audio en una aplicación para UWP.

Para obtener más información sobre XAudio2, vea Guía de programación. Para obtener más información sobre Media Foundation, consulte 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 usa formatos de audio en tiempo de diseño ni en tiempo de ejecución independientes y no usa compatibilidad con compresión XAudio2 ADPCM. Para obtener más información sobre la compresión de ADPCM en XAudio2, consulte Introducción a ADPCM.

El método Audio::CreateResources, al que se llama desde MarbleMazeMain::LoadDeferredResources, carga las secuencias de audio de los archivos, inicializa los objetos del motor XAudio2 y crea las voces de origen, submezcla y mastering.

Creación de 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 IXAudio2 en una aplicación para UWP difiere de una aplicación de escritorio de dos maneras. En primer lugar, no tiene que 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 de maestro

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 de maestro para los sonidos del juego, salvo que especifica AudioCategory_GameEffects para el parámetro StreamCategory , que es 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 effect se representan en XAudio2 mediante objetos de procesamiento de audio multiplataforma (XAPO). Para obtener más información sobre XAPO, consulte Información general sobre XAPO.

Al crear una cadena de efectos, siga estos pasos:

  1. Cree el objeto de efecto.

  2. Rellene una estructura XAUDIO2_EFFECT_DESCRIPTOR con datos de efecto.

  3. Rellene una estructura de XAUDIO2_EFFECT_CHAIN con datos.

  4. Aplique la cadena de efectos a una voz.

  5. Rellene una estructura de parámetros de efecto y aplíquela al efecto.

  6. 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 para crear un objeto ComPtr<IUnknown> , soundEffectXAPO, que actúa como voz de submezcla para el efecto reverberador.

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 se inicia en el 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 XAUDIO2_EFFECT_DESCRIPTOR objetos que participan en el efecto. En el ejemplo siguiente se muestra cómo el método Audio::CreateReverb especifica el efecto que se va a implementar 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 submezcla para el efecto. Especifica el objeto XAUDIO2_EFFECT_CHAIN , soundEffectChain, para que el parámetro pEffectChain asocie la cadena de efectos a 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 de campo cercano.

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 de campo cercano, EarlyDiffusion en 4 para simular superficies planas cercanas y LateDiffusion en 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 usar el método ReverbConvertI3DL2ToNative para usar parámetros I3DL2 estándar del sector (Guías de representación de audio 2.0 interactivas).

En el ejemplo siguiente se muestra cómo Audio::CreateReverb establece los parámetros de reverberación. newSubmix es un objeto IXAudio2SubmixVoice**. parameters es un objeto XAUDIO2FX_REVERB_PARAMETERS*.

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

El método Audio::CreateReverb finaliza habilitando el efecto mediante IXAudio2Voice::EnableEffect si se establece la marca 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 se atenua entre las dos reverberaciones (simulando la transición de estar cerca de una pared a estar en una sala grande) o enmuta ambas reverbs 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 método Audio::CreateReverb 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. Así es como 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.h y 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 IMFSourceReader , m_reader, lee los datos multimedia del archivo especificado por url.

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

A continuación, el método MediaStreamer::Initialize crea un objeto IMFMediaType mediante MFCreateMediaType para describir el formato de la secuencia 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; vea 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 de rodadura de mármol.

// 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 voces de origen

Marble Maze crea voces de origen XAudio2 para reproducir cada uno de sus sonidos de juego y música en 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 contiene el objeto IXAudio2SourceVoice para un efecto y también define otros datos relacionados con el efecto, como el búfer de audio. Audio.h define la enumeración SoundEvent . Marble Maze usa esta enumeración para identificar cada sonido del juego. La clase Audio también usa esta enumeración para indizar la matriz de objetos 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 de archivo Descripción
RollingEvent MarbleRoll.wav Tocó como rollos de mármol.
FallingEvent MarbleFall.wav Se reproduce cuando la canica cae del laberinto.
CollisionEvent MarbleHit.wav Se reproduce cuando la canica entra en conflicto con el laberinto.
CheckpointEvent Checkpoint.wav Se reproduce cuando la canica pasa sobre un punto de control.
MenuChangeEvent MenuChange.wav Se reproduce cuando el usuario cambia el elemento de menú actual.
MenuSelectedEvent 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 de destino de otra voz y especifica si se debe usar un filtro. Marble Maze llama al método Audio::SetSoundEffectFilter para usar los filtros para cambiar el sonido de la bola a medida que se tira. 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 define el formato de los búferes de audio que se envían 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 el estado detenido. Marble Maze inicia la música de fondo en el bucle del juego. La primera llamada a MarbleMazeMain::Update llama 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 para 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 los bucles de música de fondo, la voz de origen debe actualizarse periódicamente con datos para que la música continúe reproduciendo.

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 a 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 cola.

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 pops en el audio porque el motor de audio no tiene suficientes datos de audio almacenados en búfer 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, SetSoundEffectPitch y SetSoundEffectFilter para permitir que el juego controle cuándo se reproducen y detienen los sonidos, y para controlar propiedades de sonido como volumen y tono. Por ejemplo, si la canica cae fuera del laberinto, MarbleMazeMain::Update llama al método Audio::P laySoundEffect para reproducir el sonido FallingEvent .

m_audio.PlaySoundEffect(FallingEvent);

El método Audio::P laySoundEffect llama al método IXAudio2SourceVoice::Start para comenzar la reproducción del sonido. Si ya se ha llamado al método IXAudio2SourceVoice::Start , no se vuelve a iniciar. Audio::P laySoundEffect 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;
}

Para sonidos que no sean graduales, el método Audio::P laySoundEffect llama a IXAudio2SourceVoice::GetState para determinar el número de búferes que está reproduciendo la voz de origen. Llama a IXAudio2SourceVoice::SubmitSourceBuffer para agregar los datos de audio del sonido a la cola de entrada de voz si no hay búferes activos. El método Audio::P laySoundEffect también permite reproducir el sonido de colisión dos veces en secuencia. Esto ocurre, por ejemplo, cuando la canica entra en conflicto 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 inicia la reproducción en bucle la primera vez que se llama a Audio::P laySoundEffect para este evento. Para simplificar la lógica de reproducción para el sonido gradual, Marble Mazemuta 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 cambia el sonido estableciendo su volumen en cero cuando se detiene la canica.

// 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);
}

Reacción a eventos de suspensión y reanudación

La estructura de la aplicación Marble Maze describe cómo Marble Maze admite la 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.

Se llama al método Audio::SuspendAudio cuando se suspende el juego. Este método llama al método IXAudio2::StopEngine para detener todo el audio. Aunque IXAudio2::StopEngine detiene inmediatamente toda la salida de audio, conserva el gráfico 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. Dado que la llamada a IXAudio2::StopEngine conserva el gráfico 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 usa devoluciones de llamada del motor para controlar los errores del motor XAudio2, como cuando cambia el dispositivo de audio. Una causa probable de un cambio de dispositivo es cuando el usuario del juego se conecta o desconecta los auriculares. Se recomienda implementar la devolución de llamada del motor que controla 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 registrarse para devoluciones de llamada, Marble Maze llama al método IXAudio2::RegisterForCallbacks en Audio::CreateResources, después de crear el objeto IXAudio2 para el 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 la marca 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 del juego cada fotograma, comprueba primero la marca 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 la marca m_engineExperiencedCriticalError para protegerse frente a las 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 desplazamiento o colisión cuando se establece esta marca. La aplicación intenta reparar el motor de audio cada fotograma si es necesario; Sin embargo, la marca de m_engineExperiencedCriticalError siempre puede establecerse si el equipo no tiene un dispositivo de audio o los auriculares están desconectados y no hay ningún otro dispositivo de audio disponible.

Precaución

Como regla, 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 marca en la devolución de llamada OnCriticalError y, posteriormente, controla el error durante la fase de procesamiento de audio normal. Para obtener más información sobre las devoluciones de llamada XAudio2, vea Devoluciones de llamada XAudio2.

Conclusión

¡Eso encapsula la muestra 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 a 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.