Aggiungere audio

Nota

Questo argomento fa parte della serie di esercitazioni Creare un semplice gioco UWP (Universal Windows Platform) con DirectX. L'argomento in tale collegamento imposta il contesto per la serie.

In questo argomento, creiamo un motore audio semplice utilizzando le API XAudio2. Se si ha poca familiarità con XAudio2, abbiamo incluso una breve introduzione in Concetti di audio.

Nota

Se non è ancora stato scaricato il codice di gioco più recente per questo esempio, andare in Esempio di gioco Direct3D. Questo esempio fa parte di una vasta raccolta di esempi di funzionalità UWP. Per istruzioni su come scaricare l'esempio, vedere Applicazioni di esempio per lo sviluppo di Windows.

Obiettivo

Aggiungere suoni nel gioco di esempio utilizzando XAudio2.

Definire il motore audio

Nel gioco di esempio, gli oggetti audio e i comportamenti sono definiti in tre file:

  • Audio.h/.cpp: definisce l'oggetto Audio, che contiene le risorse XAudio2 per la riproduzione audio. Definisce anche il metodo per sospendere e riprendere la riproduzione audio se il gioco viene messo in pausa o disattivato.
  • MediaReader.h/.cpp: definisce i metodi per la lettura dei file audio .wav dall'archivio locale.
  • SoundEffect.h/.cpp: definisce un oggetto per la riproduzione audio nel gioco.

Panoramica

Sono note tre parti principali per la configurazione della riproduzione audio in un gioco.

  1. Creare e inizializzare le risorse audio
  2. Caricare il file audio
  3. Associare il suono all'oggetto

Sono tutte definite nel metodo Simple3DGame::Initialize. Esaminiamo quindi prima questo metodo e quindi esaminiamo altri dettagli in ciascuna delle sezioni.

Dopo la configurazione, impareremo come attivare gli effetti audio da riprodurre. Per maggiori informazioni, andare su Riprodurre il suono.

Il metodo Simple3DGame::Initialize

In Simple3DGame::Initialize, dove vengono inizializzati anche m_controller e m_renderer, impostiamo il motore audio e lo prepariamo per riprodurre suoni.

  • Creare m_audioController, ovvero un'istanza della classe Audio.
  • Creare le risorse audio necessarie tramite il metodo Audio::CreateDeviceIndependentResources. In questo caso sono stati creati due oggetti XAudio2, un oggetto motore musica e un oggetto motore suono, e una "mastering voice" per ciascuno di essi. L'oggetto motore musicale può essere utilizzato per riprodurre musica di sottofondo per il gioco. L'oggetto motore suono può essere utilizzato per riprodurre gli effetti audio del gioco. Per maggiori informazioni, vedere Creare e inizializzare le risorse audio.
  • Creare m_audioController, ovvero un'istanza della classe MediaReader. MediaReader, ovvero una classe helper per la classe SoundEffect, legge i file audio di piccole dimensioni in modo sincrono dalla posizione del file e restituisce dati audio come matrice di byte.
  • Utilizzare MediaReader::LoadMedia per caricare i file audio dalla loro posizione e creare una variabile targetHitSound per contenere i dati audio .wav caricati. Per maggiori informazioni, vedere Caricare file audio.

Gli effetti audio sono associati all'oggetto del gioco. Quindi, quando si verifica una collisione con l'oggetto del gioco, si attiva l'effetto audio da riprodurre. In questo gioco di esempio abbiamo effetti audio per le munizioni (ciò che utilizziamo per sparare ai bersagli) e per il bersaglio.

  • Nella classe GameObject è presente una proprietà HitSound che viene utilizzata per associare l'effetto audio all'oggetto.
  • Creare una nuova istanza della classe SoundEffect e inizializzarla. Durante l'inizializzazione, viene creata una voce sorgente per l'effetto audio.
  • Questa classe riproduce un suono tramite una mastering voice fornita dalla classe Audio. I dati audio vengono letti dal percorso del file tramite la classe MediaReader . Per maggiori informazioni, vedere Associare l'audio all'oggetto.

Nota

Il trigger effettivo per riprodurre l'audio è determinato dal movimento e dalla collisione di questi oggetti di gioco. Di conseguenza, la chiamata per riprodurre effettivamente questi suoni è definita nel metodo Simple3DGame::UpdateDynamics. Per maggiori informazioni, andare su Riprodurre il suono.

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]);
    }
    ...
}

Creare e inizializzare le risorse audio

  • Utilizzare XAudio2Create, un'API XAudio2, per creare due nuovi oggetti XAudio2 che definiscono i motori per musica ed effetti audio. Questo metodo restituisce un puntatore all'interfaccia IXAudio2 dell'oggetto che gestisce tutti gli stati del motore audio, il thread di elaborazione audio, il grafico vocale e altro ancora.
  • Dopo aver creato un'istanza dei motori, utilizzare IXAudio2::CreateMasteringVoice per creare una mastering voice per ognuno degli oggetti del motore audio.

Per maggiori informazioni, andare a Come inizializzare XAudio2.

Metodo 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;
}

Caricare il file audio

Nel gioco di esempio il codice per la lettura dei file in formato audio è definito in MediaReader.h/cpp__. Per leggere un file audio .wav codificato, chiamare MediaReader::LoadMedia, trasferendo il nome file del .wav come parametro di input.

Metodo MediaReader::LoadMedia

Questo metodo utilizza le API di Media Foundation per leggere nel file audio .wav come buffer PCM (Pulse Code Modulation).

Configurare il lettore di origine

  1. Utilizzare MFCreateSourceReaderFromURL per creare un lettore multimediale di origine (IMFSourceReader).
  2. Utilizzare MFCreateMediaType per creare un oggetto di tipo di supporto (IMFMediaType) (mediaType). Rappresenta una descrizione di un formato multimediale.
  3. Specificare che l'output decodificato di mediaType è audio PCM, ovvero un tipo di audio utilizzabile da XAudio2 .
  4. Impostare il tipo di supporto di output decodificato per il lettore di origine chiamando IMFSourceReader::SetCurrentMediaType.

Per maggiori informazioni sul motivo per cui utilizziamo il lettore di origine, andare a Lettore di origine.

Descrivere il formato dei dati del flusso audio

  1. Utilizzare IMFSourceReader::GetCurrentMediaType per ottenere il tipo di supporto corrente per il flusso.
  2. Utilizzare IMFMediaType::MFCreateWaveFormatExFromMFMediaType per convertire il tipo di supporto audio corrente in un buffer WAVEFORMATEX, utilizzando i risultati dell'operazione precedente come input. Questa struttura specifica il formato dati del flusso audio a forma d'onda che viene utilizzato dopo il caricamento dell'audio.

È possibile utilizzare il formato WAVEFORMATEX per descrivere il buffer PCM. Rispetto alla struttura WAVEFORMATEXTENSIBLE, è possibile utilizzarla esclusivamente per descrivere un sottoinsieme di formati audio a forma d'onda. Per maggiori informazioni sulle differenze tra WAVEFORMATEX e WAVEFORMATEXTENSIBLE, vedere Descrittori del formato audio a forma d'onda estensibili.

Leggere il flusso audio

  1. Ottenere la durata, in secondi, del flusso audio chiamando IMFSourceReader::GetPresentationAttribute e quindi convertire la durata in byte.
  2. Leggere il file audio come flusso chiamando IMFSourceReader::ReadSample. ReadSample legge il campione successivo dalla sorgente multimediale.
  3. Utilizzare IMFSample::ConvertToContiguousBuffer per copiare il contenuto del buffer del campione audio (sample) in una matrice (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;
}

Associare il suono all'oggetto

L'associazione di suoni all'oggetto avviene quando il gioco viene inizializzato nel metodo Simple3DGame::Initialize.

Riepilogo:

  • Nella classe GameObject è presente una proprietà HitSound che viene utilizzata per associare l'effetto audio all'oggetto.
  • Creare una nuova istanza dell'oggetto di classe SoundEffect e associarla all'oggetto di gioco. Questa classe riproduce un suono usando API XAudio2. Utilizza una mastering voice fornita dalla classe Audio. I dati audio possono essere letti dal percorso del file tramite la classe MediaReader.

SoundEffect::Initialize viene utilizzato per inizializzare l'istanza SoundEffect con i seguenti parametri di input: puntatore all'oggetto motore audio (oggetti IXAudio2 creati nel metodo Audio::CreateDeviceIndependentResources), puntatore al formato del file .wav tramite MediaReader::GetOutputWaveFormatEx e i dati audio caricati tramite il metodo MediaReader::LoadMedia. Durante l'inizializzazione, viene creata anche una voce sorgente per l'effetto audio.

Il metodo 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;
}

Riprodurre il suono

I trigger per riprodurre gli effetti audio sono definiti nel metodo Simple3DGame::UpdateDynamics in quanto qui è dove viene aggiornato il movimento degli oggetti e viene determinata la collisione tra gli oggetti.

Poiché l'interazione tra gli oggetti differisce notevolmente, a seconda del gioco, non esamineremo le dinamiche degli oggetti di gioco in questa sede. Se si è interessati a comprenderne l'implementazione, andare al metodo Simple3DGame::UpdateDynamics.

In linea di principio, quando si verifica una collisione, questa attiva la riproduzione dell'effetto audio chiamando SoundEffect::P laySound. Questo metodo arresta tutti gli effetti audio correntemente in riproduzione e accoda il buffer in memoria con i dati audio desiderati. Utilizza la voce di origine per impostare il volume, inviare i dati audio e avviare la riproduzione.

Il metodo SoundEffect::PlaySound

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

Il metodo Simple3DGame::UpdateDynamics

Il metodo Simple3DGame::UpdateDynamics gestisce l'interazione e la collisione tra oggetti di gioco. Quando gli oggetti si scontrano (o si intersecano), attiva l'effetto audio associato per riprodurlo.

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
}

Passaggi successivi

Abbiamo trattato il framework UWP, la grafica, i controlli, l'interfaccia utente e l'audio di un gioco in Windows 10. La parte successiva di questa esercitazione, Estensione del gioco di esempio, illustra altre opzioni che è possibile utilizzare durante lo sviluppo di un gioco.

Concetti relativi all'audio

Per lo sviluppo di giochi in Windows 10, utilizzare XAudio2 versione 2.9. Questa versione viene fornita con Windows 10. Per maggiori informazioni, andare a Versioni XAudio2.

XAudio2 è un'API di basso livello che fornisce elaborazione dei segnali e la base di combinazione. Per maggiori informazioni, vedere Concetti chiave di XAudio2.

Voci di XAudio2

Esistono 3 tipi di oggetti voce XAudio2: sorgente, submix e mastering voice. Le voci sono gli oggetti utilizzati da XAudio2 per elaborare, manipolare e riprodurre dati audio.

  • Le voci sorgente operano su dati audio forniti dal client.
  • Le voci sorgente e submix inviano il loro output a una o più voci submix o mastering.
  • Le voci submix e mastering miscelano l'audio proveniente da tutte le voci che le alimentano e operano sul risultato.
  • Le mastering voice ricevono dati delle voci sorgente e submix e li inviano all'hardware audio.

Per maggiori informazioni, andare a Voci di XAudio2.

Grafico audio

Il grafico audio è una raccolta di voci di XAudio2. L'audio ha inizio da un lato di un grafico audio in voci sorgente, passa facoltativamente attraverso una o più voci submix e termina in una mastering voice. Un grafico audio conterrà una voce sorgente per ogni suono correntemente riprodotto, zero o più voci submix e una sola mastering voice. Il grafico audio più semplice, e il minimo necessario per creare un rumore in XAudio2, è un'unica voce sorgente che restituisce direttamente una mastering voice. Per maggiori informazioni, andare a Grafici audio.

Altre letture

File audio .h chiave

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:
    ...
};