Compartir a través de


Agregar sonido

Nota:

Este tema forma parte de la serie de tutoriales Crear un sencillo juego para la Plataforma Universal de Windows (UWP) con DirectX. El tema de ese vínculo establece el contexto de la serie.

En este tema, creamos un motor de sonido sencillo mediante API de XAudio2. Si no está familiarizado con XAudio2, hemos incluido una breve introducción en Conceptos de audio.

Nota:

Si no has descargado el código más reciente para este juego de ejemplo, ve a juego de ejemplo Direct3D. Este ejemplo forma parte de una gran colección de ejemplos de características de UWP. Para obtener instrucciones sobre cómo descargar el ejemplo, consulte Aplicaciones de ejemplo para el desarrollo de Windows.

Objetivo

Agregue sonidos al juego de muestra mediante XAudio2.

Definición del motor de audio

En el juego de ejemplo, los objetos y comportamientos de audio se definen en tres archivos:

  • Audio.h/.cpp: define el objeto Audio , que contiene los recursos XAudio2 para la reproducción de sonido. También define el método para suspender y reanudar la reproducción de audio si el juego está en pausa o desactivado.
  • MediaReader.h/.cpp: define los métodos para leer archivos de audio .wav del almacenamiento local.
  • SoundEffect.h/.cpp: define un objeto para la reproducción de sonido en el juego.

Información general

Hay tres partes principales para configurar la reproducción de audio en el juego.

  1. Creación e inicialización de los recursos de audio
  2. Carga de archivo de audio
  3. Asociar sonido al objeto

Se definen todos en el método Simple3DGame::Initialize. Por lo tanto, vamos a examinar primero este método y, a continuación, vamos a profundizar en más detalles en cada una de las secciones.

Después de configurar, aprendemos a desencadenar los efectos de sonido para reproducir. Para obtener más información, dirígete a Reproducir el sonido.

Método Simple3DGame::Initialize

En Simple3DGame::Initialize, donde también se inicializan m_controller y m_renderer , configuramos el motor de audio y lo preparamos para reproducir sonidos.

  • Cree m_audioController, que es una instancia de la clase Audio.
  • Cree los recursos de audio necesarios mediante el método Audio::CreateDeviceIndependentResources . Aquí se crearon dos objetos de XAudio2: un motor de música, un motor de sonido y una voz maestra para cada uno de ellos. El objeto de motor de música se puede usar para reproducir música de fondo para tu juego. El motor de sonido se puede usar para reproducir efectos de sonido en tu juego. Para obtener más información, consulta Crear e inicializar los recursos de audio.
  • Cree mediaReader, que es una instancia de la clase MediaReader. MediaReader, que es una clase auxiliar para la clase SoundEffect , lee archivos de audio pequeños de forma sincrónica desde la ubicación del archivo y devuelve datos de sonido como una matriz de bytes.
  • Use MediaReader::LoadMedia para cargar archivos de sonido desde su ubicación y crear una variable targetHitSound para contener los datos de sonido cargados .wav. Para obtener más información, consulta Cargar archivo de audio.

Los efectos de sonido están asociados al objeto del juego. Por lo tanto, cuando se produce una colisión con ese objeto de juego, desencadena el efecto de sonido que se va a reproducir. En este juego de ejemplo, tenemos efectos de sonido para la munición (lo que usamos para disparar objetivos con) y para el objetivo.

  • En la clase GameObject , hay una propiedad HitSound que se usa para asociar el efecto de sonido al objeto.
  • Cree una nueva instancia de la clase SoundEffect e inicialícela. Durante la inicialización, se crea una voz de origen para el efecto de sonido.
  • Esta clase reproduce un sonido mediante una voz de masterización proporcionada desde la clase de audio . Los datos de sonido se leen desde la ubicación del archivo mediante la clase MediaReader. Para obtener más información, consulta Asociar sonido al objeto.

Nota:

El desencadenador real para reproducir el sonido viene determinado por el movimiento y la colisión de estos objetos de juego. Por lo tanto, la llamada a reproducir realmente estos sonidos se define en el método Simple3DGame::UpdateDynamics . Para obtener más información, dirígete a Reproducir el sonido.

void Simple3DGame::Initialize(
    _In_ std::shared_ptr<MoveLookController> const& controller,
    _In_ std::shared_ptr<GameRenderer> const& renderer
    )
{
    // The following member is defined in the header file:
    // Audio m_audioController;

    ...

    // Create the audio resources needed.
    // Two XAudio2 objects are created - one for music engine,
    // the other for sound engine. A mastering voice is also
    // created for each of the objects.
    m_audioController.CreateDeviceIndependentResources();

    m_ammo.resize(GameConstants::MaxAmmo);

    ...

    // Create a media reader which is used to read audio files from its file location.
    MediaReader mediaReader;
    auto targetHitSoundX = mediaReader.LoadMedia(L"Assets\\hit.wav");

    // Instantiate the targets for use in the game.
    // Each target has a different initial position, size, and orientation.
    // But share a common set of material properties.
    for (int a = 1; a < GameConstants::MaxTargets; a++)
    {
        ...
        // Create a new sound effect object and associate it
        // with the game object's (target) HitSound property.
        target->HitSound(std::make_shared<SoundEffect>());

        // Initialize the sound effect object with
        // the sound effect engine, format of the audio wave, and audio data
        // During initialization, source voice of this sound effect is also created.
        target->HitSound()->Initialize(
            m_audioController.SoundEffectEngine(),
            mediaReader.GetOutputWaveFormatEx(),
            targetHitSoundX
            );
        ...
    }

    // Instantiate a set of spheres to be used as ammunition for the game
    // and set the material properties of the spheres.
    auto ammoHitSound = mediaReader.LoadMedia(L"Assets\\bounce.wav");

    for (int a = 0; a < GameConstants::MaxAmmo; a++)
    {
        m_ammo[a] = std::make_shared<Sphere>();
        m_ammo[a]->Radius(GameConstants::AmmoRadius);
        m_ammo[a]->HitSound(std::make_shared<SoundEffect>());
        m_ammo[a]->HitSound()->Initialize(
            m_audioController.SoundEffectEngine(),
            mediaReader.GetOutputWaveFormatEx(),
            ammoHitSound
            );
        m_ammo[a]->Active(false);
        m_renderObjects.push_back(m_ammo[a]);
    }
    ...
}

Creación e inicialización de los recursos de audio

  • Use XAudio2Create, una API de XAudio2, para crear dos nuevos objetos XAudio2 que definen los motores de música y efectos de sonido. Este método devuelve un puntero a la interfaz IXAudio2 del objeto que administra todos los estados del motor de audio, el subproceso de procesamiento de audio, el gráfico de voz, etc.
  • Después de crear una instancia de los motores de sonido, use IXAudio2::CreateMasteringVoice para crear una voz de masterización para cada uno de los objetos del motor de sonido.

Para obtener más información, vaya a Cómo: Inicializar XAudio2.

Método Audio::CreateDeviceIndependentResources

void Audio::CreateDeviceIndependentResources()
{
    UINT32 flags = 0;

    winrt::check_hresult(
        XAudio2Create(m_musicEngine.put(), flags)
        );

    HRESULT hr = m_musicEngine->CreateMasteringVoice(&m_musicMasteringVoice);
    if (FAILED(hr))
    {
        // Unable to create an audio device
        m_audioAvailable = false;
        return;
    }

    winrt::check_hresult(
        XAudio2Create(m_soundEffectEngine.put(), flags)
        );

    winrt::check_hresult(
        m_soundEffectEngine->CreateMasteringVoice(&m_soundEffectMasteringVoice)
        );

    m_audioAvailable = true;
}

Cargar archivo de audio

En el juego de ejemplo, el código para leer archivos de formato de audio se define en MediaReader.h/cpp__. Para leer un archivo de audio codificado .wav, llame a MediaReader::LoadMedia y pase el nombre de archivo del .wav como parámetro de entrada.

Método MediaReader::LoadMedia

Este método usa las API de Media Foundation para leer en el archivo de audio .wav como un búfer de modulación de código de pulso (PCM).

Configuración del Lector de origen

  1. Use MFCreateSourceReaderFromURL para crear un lector de origen multimedia (IMFSourceReader).
  2. Utilice MFCreateMediaType para crear un tipo de medio (IMFMediaType) objeto (mediaType). Representa una descripción de un formato multimedia.
  3. Especifique que la salida descodificada del mediaTypees de audio PCM, el cual es un tipo de audio que XAudio2 puede usar.
  4. Establece el tipo de medio de salida descodificado para el lector de origen llamando a IMFSourceReader::SetCurrentMediaType.

Para obtener más información sobre por qué usamos el Lector de origen, vaya a Lector de origen.

Describir el formato de datos de la secuencia de audio

  1. Utilice IMFSourceReader::GetCurrentMediaType para obtener el tipo de medio actual para el flujo.
  2. Utilice IMFMediaType::MFCreateWaveFormatExFromMFMediaType para convertir el tipo de medio de audio actual en un búfer de WAVEFORMATEX, utilizando los resultados de la operación anterior como entrada. Esta estructura especifica el formato de datos del flujo de audio WAV que se usa una vez que se haya cargado el audio.

El formato WAVEFORMATEX se puede usar para describir el búfer PCM. En comparación con la estructura WAVEFORMATEXTENSIBLE, solo se puede usar para describir un subconjunto de formatos de onda de audio. Para obtener más información sobre las diferencias entre WAVEFORMATEX y WAVEFORMATEXTENSIBLE, consulta Descriptores extensibles de Wave-Format.

Leer la secuencia de audio

  1. Obtenga la duración, en segundos, de la secuencia de audio llamando a IMFSourceReader::GetPresentationAttribute y, a continuación, convierte la duración en bytes.
  2. Lea el archivo de audio como una secuencia llamando a IMFSourceReader::ReadSample. readSample lee el ejemplo siguiente del origen multimedia.
  3. Utilice IMFSample::ConvertToContiguousBuffer para copiar el contenido del búfer de muestra de audio (muestra) en una matriz (mediaBuffer).
std::vector<byte> MediaReader::LoadMedia(_In_ winrt::hstring const& filename)
{
    winrt::check_hresult(
        MFStartup(MF_VERSION)
        );

    // Creates a media source reader.
    winrt::com_ptr<IMFSourceReader> reader;
    winrt::check_hresult(
        MFCreateSourceReaderFromURL(
        (m_installedLocationPath + filename).c_str(),
            nullptr,
            reader.put()
            )
        );

    // Set the decoded output format as PCM.
    // XAudio2 on Windows can process PCM and ADPCM-encoded buffers.
    // When using MediaFoundation, this sample always decodes into PCM.
    winrt::com_ptr<IMFMediaType> mediaType;
    winrt::check_hresult(
        MFCreateMediaType(mediaType.put())
        );

    // Define the major category of the media as audio. For more info about major media types,
    // go to: https://msdn.microsoft.com/library/windows/desktop/aa367377.aspx
    winrt::check_hresult(
        mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)
        );

    // Define the sub-type of the media as uncompressed PCM audio. For more info about audio sub-types,
    // go to: https://msdn.microsoft.com/library/windows/desktop/aa372553.aspx
    winrt::check_hresult(
        mediaType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)
        );

    // Sets the media type for a stream. This media type defines that format that the Source Reader 
    // produces as output. It can differ from the native format provided by the media source.
    // For more info, go to https://msdn.microsoft.com/library/windows/desktop/dd374667.aspx
    winrt::check_hresult(
        reader->SetCurrentMediaType(static_cast<uint32_t>(MF_SOURCE_READER_FIRST_AUDIO_STREAM), 0, mediaType.get())
        );

    // Get the current media type for the stream.
    // For more info, go to:
    // https://msdn.microsoft.com/library/windows/desktop/dd374660.aspx
    winrt::com_ptr<IMFMediaType> outputMediaType;
    winrt::check_hresult(
        reader->GetCurrentMediaType(static_cast<uint32_t>(MF_SOURCE_READER_FIRST_AUDIO_STREAM), outputMediaType.put())
        );

    // Converts the current media type into the WaveFormatEx buffer structure.
    UINT32 size = 0;
    WAVEFORMATEX* waveFormat;
    winrt::check_hresult(
        MFCreateWaveFormatExFromMFMediaType(outputMediaType.get(), &waveFormat, &size)
        );

    // Copies the waveFormat's block of memory to the starting address of the m_waveFormat variable in MediaReader.
    // Then free the waveFormat memory block.
    // For more info, go to https://msdn.microsoft.com/library/windows/desktop/aa366535.aspx and
    // https://msdn.microsoft.com/library/windows/desktop/ms680722.aspx
    CopyMemory(&m_waveFormat, waveFormat, sizeof(m_waveFormat));
    CoTaskMemFree(waveFormat);

    PROPVARIANT propVariant;
    winrt::check_hresult(
        reader->GetPresentationAttribute(static_cast<uint32_t>(MF_SOURCE_READER_MEDIASOURCE), MF_PD_DURATION, &propVariant)
        );

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

    std::vector<byte> fileData(maxStreamLengthInBytes);

    winrt::com_ptr<IMFSample> sample;
    winrt::com_ptr<IMFMediaBuffer> mediaBuffer;
    DWORD flags = 0;

    int positionInData = 0;
    bool done = false;
    while (!done)
    {
        // Read audio data.
        ...
    }

    return fileData;
}

Asociar sonido al objeto

La asociación de sonidos al objeto tiene lugar cuando el juego se inicializa, en el método Simple3DGame::Initialize .

Resumen

  • En la clase GameObject , hay una propiedad HitSound que se usa para asociar el efecto de sonido al objeto.
  • Cree una nueva instancia del objeto de clase SoundEffect y asócielo al objeto de juego. Esta clase reproduce un sonido mediante API de XAudio2. Utiliza una voz de masterización proporcionada por la clase de Audio . Los datos de sonido se pueden leer desde la localización del archivo mediante la clase MediaReader.

SoundEffect::Initialize se usa para inicializar la instancia de SoundEffect con los siguientes parámetros de entrada: puntero al objeto del motor de sonido (objetos IXAudio2 creados en el método Audio::CreateDeviceIndependentResources), puntero al formato del archivo .wav mediante MediaReader::GetOutputWaveFormatExy los datos de sonido cargados mediante el método MediaReader::LoadMedia. Durante la inicialización, también se crea la voz de origen para el efecto de sonido.

Método SoundEffect::Initialize

void SoundEffect::Initialize(
    _In_ IXAudio2* masteringEngine,
    _In_ WAVEFORMATEX* sourceFormat,
    _In_ std::vector<byte> const& soundData)
{
    m_soundData = soundData;

    if (masteringEngine == nullptr)
    {
        // Audio is not available so just return.
        m_audioAvailable = false;
        return;
    }

    // Create a source voice for this sound effect.
    winrt::check_hresult(
        masteringEngine->CreateSourceVoice(
            &m_sourceVoice,
            sourceFormat
            )
        );
    m_audioAvailable = true;
}

Reproducir el sonido

Los desencadenadores para reproducir efectos de sonido se definen en el método Simple3DGame::UpdateDynamics porque aquí es donde se actualiza el movimiento de los objetos y se determina la colisión entre objetos.

Dado que la interacción entre objetos difiere en gran medida, dependiendo del juego, no vamos a discutir la dinámica de los objetos del juego aquí. Si está interesado en comprender su implementación, vaya al método Simple3DGame::UpdateDynamics.

En principio, cuando se produce una colisión, desencadena la reproducción del efecto de sonido llamando a SoundEffect::PlaySound. Este método detiene los efectos de sonido que se están reproduciendo actualmente y pone el búfer en memoria en espera con los datos de sonido deseados. Usa la voz de origen para establecer el volumen, enviar datos de sonido e iniciar la reproducción.

El método SoundEffect::PlaySound

  • Usa el objeto de voz de origen m_sourceVoice para iniciar la reproducción del búfer de datos de sonido m_soundData
  • Crea un XAUDIO2_BUFFER, al que proporciona una referencia al búfer de datos de sonido y, a continuación, lo envía con una llamada a IXAudio2SourceVoice::SubmitSourceBuffer.
  • Con los datos de sonido en cola, SoundEffect::PlaySound comienza la reproducción al llamar a IXAudio2SourceVoice::Start.
void SoundEffect::PlaySound(_In_ float volume)
{
    XAUDIO2_BUFFER buffer = { 0 };

    if (!m_audioAvailable)
    {
        // Audio is not available so just return.
        return;
    }

    // Interrupt sound effect if it is currently playing.
    winrt::check_hresult(
        m_sourceVoice->Stop()
        );
    winrt::check_hresult(
        m_sourceVoice->FlushSourceBuffers()
        );

    // Queue the memory buffer for playback and start the voice.
    buffer.AudioBytes = (UINT32)m_soundData.size();
    buffer.pAudioData = m_soundData.data();
    buffer.Flags = XAUDIO2_END_OF_STREAM;

    winrt::check_hresult(
        m_sourceVoice->SetVolume(volume)
        );
    winrt::check_hresult(
        m_sourceVoice->SubmitSourceBuffer(&buffer)
        );
    winrt::check_hresult(
        m_sourceVoice->Start()
        );
}

Método Simple3DGame::UpdateDynamics

El método Simple3DGame::UpdateDynamics se encarga de la interacción y colisión entre objetos de juego. Cuando los objetos colisionan (o intersectan), se desencadena el efecto de sonido asociado.

void Simple3DGame::UpdateDynamics()
{
    ...
    // Check for collisions between ammo.
#pragma region inter-ammo collision detection
if (m_ammoCount > 1)
{
    ...
    // Check collision between instances One and Two.
    ...
    if (distanceSquared < (GameConstants::AmmoSize * GameConstants::AmmoSize))
    {
        // The two ammo are intersecting.
        ...
        // Start playing the sounds for the impact between the two balls.
        m_ammo[one]->PlaySound(impact, m_player->Position());
        m_ammo[two]->PlaySound(impact, m_player->Position());
    }
}
#pragma endregion

#pragma region Ammo-Object intersections
    // Check for intersections between the ammo and the other objects in the scene.
    // ...
    // Ball is in contact with Object.
    // ...

    // Make sure that the ball is actually headed towards the object. At grazing angles there
    // could appear to be an impact when the ball is actually already hit and moving away.

    if (impact > 0.0f)
    {
        ...
        // Play the sound associated with the Ammo hitting something.
        m_objects[i]->PlaySound(impact, m_player->Position());

        if (m_objects[i]->Target() && !m_objects[i]->Hit())
        {
            // The object is a target and isn't currently hit, so mark
            // it as hit and play the sound associated with the impact.
            m_objects[i]->Hit(true);
            m_objects[i]->HitTime(timeTotal);
            m_totalHits++;

            m_objects[i]->PlaySound(impact, m_player->Position());
        }
        ...
    }
#pragma endregion

#pragma region Apply Gravity and world intersection
            // Apply gravity and check for collision against enclosing volume.
            ...
                if (position.z < limit)
                {
                    // The ammo instance hit the a wall in the min Z direction.
                    // Align the ammo instance to the wall, invert the Z component of the velocity and
                    // play the impact sound.
                    position.z = limit;
                    m_ammo[i]->PlaySound(-velocity.z, m_player->Position());
                    velocity.z = -velocity.z * GameConstants::Physics::GroundRestitution;
                }
                ...
#pragma endregion
}

Pasos siguientes

Hemos tratado el marco de UWP, gráficos, controles, interfaz de usuario y audio de un juego de Windows 10. La siguiente parte de este tutorial, Ampliando el juego de muestra, explica otras opciones que se pueden usar al momento de desarrollar un juego.

Conceptos de audio

Para el desarrollo de juegos de Windows 10, usa XAudio2 versión 2.9. Esta versión se incluye con Windows 10. Para obtener más información, vaya a XAudio2 Versions.

AudioX2 es una API de bajo nivel que proporciona base de procesamiento y mezcla de señales. Para obtener más información, consulta Conceptos clave de XAudio2.

Voces de XAudio2

Hay tres tipos de objetos de voz XAudio2: voces de origen, submezcla y maestro. Las voces son los objetos que XAudio2 usan para procesar, manipular y reproducir datos de audio.

  • Las voces de origen operan con los datos de audio proporcionados por el cliente.
  • Las voces de origen y submezcla envían su salida a una o varias voces de submezcla o de masterización.
  • Las voces de submezcla y de masterización mezclan el audio de todas las voces que las alimentan y operan en el resultado.
  • Las voces maestras reciben datos de las voces de origen y las voces de submezcla y envían esos datos al hardware de audio.

Para obtener más información, vaya a voces XAudio2.

Gráfico de audio

El grafo de audio es una colección de voces XAudio2. El audio comienza en un extremo de un grafo de audio en voces de origen, pasa opcionalmente a través de una o varias voces de submezcla y termina en una voz de masterización. Un gráfico de audio contendrá una voz de origen para cada sonido que se reproduce actualmente, cero o más voces de submezcla y una voz de masterización. El gráfico de audio más sencillo, y lo mínimo necesario para generar un sonido en XAudio2, es una única voz fuente conectada directamente a una voz maestra. Para obtener más información, vaya a Gráficos de audio.

Lectura adicional

Archivos .h de audio clave

Audio.h

// Audio:
// This class uses XAudio2 to provide sound output. It creates two
// engines - one for music and the other for sound effects - each as
// a separate mastering voice.
// The SuspendAudio and ResumeAudio methods can be used to stop
// and start all audio playback.

class Audio
{
public:
    Audio();

    void Initialize();
    void CreateDeviceIndependentResources();
    IXAudio2* MusicEngine();
    IXAudio2* SoundEffectEngine();
    void SuspendAudio();
    void ResumeAudio();

private:
    ...
};

MediaReader.h

// MediaReader:
// This is a helper class for the SoundEffect class. It reads small audio files
// synchronously from the package installed folder and returns sound data as a
// vector of bytes.

class MediaReader
{
public:
    MediaReader();

    std::vector<byte> LoadMedia(_In_ winrt::hstring const& filename);
    WAVEFORMATEX* GetOutputWaveFormatEx();

private:
    winrt::Windows::Storage::StorageFolder  m_installedLocation{ nullptr };
    winrt::hstring                          m_installedLocationPath;
    WAVEFORMATEX                            m_waveFormat;
};

SoundEffect.h

// SoundEffect:
// This class plays a sound using XAudio2. It uses a mastering voice provided
// from the Audio class. The sound data can be read from disk using the MediaReader
// class.

class SoundEffect
{
public:
    SoundEffect();

    void Initialize(
        _In_ IXAudio2* masteringEngine,
        _In_ WAVEFORMATEX* sourceFormat,
        _In_ std::vector<byte> const& soundData
        );

    void PlaySound(_In_ float volume);

private:
    ...
};