Ajout d’audio dans l’exemple Marble Maze
Ce document décrit les principales pratiques à prendre en compte lorsque vous travaillez avec l’audio et montre comment Marble Maze applique ces pratiques. Marble Maze utilise Microsoft Media Foundation pour charger des ressources audio à partir de fichiers, et XAudio2 pour mélanger et lire de l’audio et appliquer des effets à l’audio.
Marble Maze joue de la musique en arrière-plan, et utilise également des sons de jeu pour indiquer des événements de jeu, tels que lorsque la bille frappe un mur. Une partie importante de l’implémentation est que Marble Maze utilise une réverbération, ou un écho, pour simuler le son d’une bille lorsqu’il rebondit. L’implémentation de l’effet de réverbération provoque des échos pour vous atteindre plus rapidement et fort dans les petites salles ; les échos sont plus calmes et vous atteignent plus lentement dans les chambres plus grandes.
Remarque
L’échantillon de code qui correspond à ce document se trouve dans l’échantillon de jeu Marble Maze DirectX.
Voici quelques-uns des points clés abordés par ce document lorsque vous travaillez avec l’audio dans votre jeu :
Envisagez d’utiliser Media Foundation pour décoder les ressources audio et XAudio2 pour lire l’audio. Toutefois, si vous disposez d’un mécanisme de chargement de ressources audio existant qui fonctionne dans une application plateforme Windows universelle (UWP), vous pouvez l’utiliser.
Un graphique audio contient une voix source pour chaque son actif, zéro ou plusieurs voix de sous-mélange et une voix de mastering. Les voix sources peuvent se nourrir de voix submixées et/ou de la voix de mastering. Les voix de sous-mélange se nourrissent d’autres voix de sous-mélange ou de la voix de mastering.
Si vos fichiers de musique d’arrière-plan sont volumineux, envisagez de diffuser votre musique en mémoires tampons plus petites afin d’utiliser moins de mémoire.
S’il est judicieux de le faire, suspendez la lecture audio lorsque l’application perd le focus ou la visibilité, ou est suspendue. Reprendre la lecture lorsque votre application reprend le focus, devient visible ou reprend.
Définissez les catégories audio pour refléter le rôle de chaque son. Par exemple, vous utilisez généralement AudioCategory_GameMedia pour l’audio d’arrière-plan du jeu et AudioCategory_GameEffects pour les effets sonores.
Gérez les changements d’appareil, y compris les écouteurs, en libérant et en recréant toutes les ressources audio et interfaces.
Déterminez s’il faut compresser des fichiers audio lors de la réduction de l’espace disque et des coûts de diffusion en continu. Sinon, vous pouvez laisser l’audio non compressé afin qu’il se charge plus rapidement.
Présentation de XAudio2 et de Microsoft Media Foundation
XAudio2 est une bibliothèque audio de bas niveau pour Windows qui prend spécifiquement en charge l’audio de jeu. Il fournit un moteur de traitement de signal numérique (DSP) et de graphique audio pour les jeux. XAudio2 s’étend sur ses prédécesseurs, DirectSound et XAudio, en prenant en charge les tendances informatiques telles que les architectures à virgule flottante SIMD et l’audio HD. Il soutient également les demandes de traitement sonore plus complexes des jeux d’aujourd’hui.
Le document XAudio2 Key Concepts explique les concepts clés pour l’utilisation de XAudio2. En bref, les concepts sont les suivants :
L’interface IXAudio2 est le cœur du moteur XAudio2. Marble Maze utilise cette interface pour créer des voix et recevoir une notification lorsque l’appareil de sortie change ou échoue.
Un processus vocal , ajuste et lit les données audio.
Une voix source est une collection de canaux audio (mono, 5.1, et ainsi de suite) et représente un flux de données audio. Dans XAudio2, une voix source est l’endroit où commence le traitement audio. En règle générale, les données sonores sont chargées à partir d’une source externe, comme un fichier ou un réseau, et sont envoyées à une voix source. Marble Maze utilise Media Foundation pour charger des données sonores à partir de fichiers. Media Foundation est présenté plus loin dans ce document.
Une voix submixée traite les données audio. Ce traitement peut inclure la modification du flux audio ou la combinaison de plusieurs flux en un seul. Marble Maze utilise des sous-mélanges pour créer l’effet de réverbération.
Une voix de mastering combine des données provenant de voix sources et de sous-mixage et envoie ces données au matériel audio.
Un graphique audio contient une voix source pour chaque son actif, zéro ou plusieurs voix submixées, et une seule voix de mastering.
Un rappel informe le code client que certains événements se sont produits dans une voix ou dans un objet moteur. En utilisant des rappels, vous pouvez réutiliser la mémoire lorsque XAudio2 est terminé avec une mémoire tampon, réagir lorsque l’appareil audio change (par exemple, lorsque vous connectez ou déconnectez des écouteurs), et bien plus encore. La gestion des casques et des modifications des appareils plus loin dans ce document explique comment Marble Maze utilise ce mécanisme pour gérer les modifications des appareils.
Marble Maze utilise deux moteurs audio (en d’autres termes, deux objets IXAudio2 ) pour traiter l’audio. Un moteur traite la musique d’arrière-plan, et l’autre moteur traite les sons de jeu.
Marble Maze doit également créer une voix de maîtrise pour chaque moteur. Rappelez-vous qu’un moteur de mastering combine des flux audio en un seul flux et envoie ce flux au matériel audio. Le flux de musique d’arrière-plan, une voix source, génère des données vers une voix de mastering et deux voix de sous-mélange. Les voix de sous-mélange effectuent l’effet de réverbération.
Media Foundation est une bibliothèque multimédia qui prend en charge de nombreux formats audio et vidéo. XAudio2 et Media Foundation se complètent. Marble Maze utilise Media Foundation pour charger des ressources audio à partir de fichiers et utilise XAudio2 pour lire l’audio. Vous n’avez pas besoin d’utiliser Media Foundation pour charger des ressources audio. Si vous disposez d’un mécanisme de chargement de ressources audio existant qui fonctionne dans les applications plateforme Windows universelle (UWP), utilisez-le. L’audio, la vidéo et la caméra traitent de plusieurs façons d’implémenter l’audio dans une application UWP.
Pour plus d’informations sur XAudio2, consultez le Guide de programmation. Pour plus d’informations sur Media Foundation, consultez Microsoft Media Foundation.
Initialisation des ressources audio
Marble Mazes utilise un fichier Audio Windows Media (.wma) pour la musique d’arrière-plan et les fichiers WAV (.wav) pour les sons de jeu. Ces formats sont pris en charge par Media Foundation. Bien que le format de fichier .wav soit pris en charge en mode natif par XAudio2, un jeu doit analyser manuellement le format de fichier pour remplir les structures de données XAudio2 appropriées. Marble Maze utilise Media Foundation pour travailler plus facilement avec des fichiers .wav. Pour obtenir la liste complète des formats multimédias pris en charge par Media Foundation, consultez Supports multimédias pris en charge dans Media Foundation. Marble Maze n’utilise pas de formats audio distincts au moment de la conception et au moment de l’exécution, et n’utilise pas la prise en charge de la compression ADPCM XAudio2. Pour plus d’informations sur la compression ADPCM dans XAudio2, consultez ADPCM Overview.
La méthode Audio ::CreateResources , appelée à partir de MarbleMazeMain ::LoadDeferredResources, charge les flux audio à partir des fichiers, initialise les objets moteur XAudio2 et crée les voix source, submix et mastering.
Création des moteurs XAudio2
Rappelez-vous que Marble Maze crée un objet IXAudio2 pour représenter chaque moteur audio qu’il utilise. Pour créer un moteur audio, appelez la méthode XAudio2Create . L’exemple suivant montre comment Marble Maze crée le moteur audio qui traite la musique d’arrière-plan.
// In Audio.h
class Audio
{
private:
IXAudio2* m_musicEngine;
// ...
}
// In Audio.cpp
void Audio::CreateResources()
{
try
{
// ...
DX::ThrowIfFailed(
XAudio2Create(&m_musicEngine)
);
// ...
}
// ...
}
Marble Maze effectue une étape similaire pour créer le moteur audio qui joue des sons de jeu.
L’utilisation de l’interface IXAudio2 dans une application UWP diffère d’une application de bureau de deux façons. Tout d’abord, vous n’avez pas besoin d’appeler CoInitializeEx avant d’appeler XAudio2Create. En outre, IXAudio2 ne prend plus en charge l’énumération d’appareils. Pour plus d’informations sur l’énumération des appareils audio, consultez Énumération des appareils.
Création des voix de maîtrise
L’exemple suivant montre comment la méthode Audio ::CreateResources crée la voix de mastering pour la musique d’arrière-plan à l’aide de la méthode IXAudio2 ::CreateMasteringVoice . Dans cet exemple, m_musicMasteringVoice est un objet IXAudio2MasteringVoice. Nous spécifions deux canaux d’entrée ; cela simplifie la logique de l’effet de réverbération.
Nous spécifions 48000 comme taux d’échantillonnage d’entrée. Nous avons choisi ce taux d’échantillonnage, car il représentait un équilibre entre la qualité audio et la quantité de traitement du processeur requis. Un taux d’échantillonnage plus élevé aurait nécessité davantage de traitement du processeur sans bénéficier d’un avantage de qualité notable.
Enfin, nous spécifions AudioCategory_GameMedia en tant que catégorie de flux audio afin que les utilisateurs puissent écouter de la musique à partir d’une autre application au fur et à mesure qu’ils jouent au jeu. Lorsqu’une application musicale est en cours de lecture, Windows désactive toutes les voix créées par l’option AudioCategory_GameMedia. L’utilisateur entend toujours des sons de jeu, car il est créé par l’option AudioCategory_GameEffects . Pour plus d’informations sur les catégories audio, consultez 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
)
);
La méthode Audio ::CreateResources effectue une étape similaire pour créer la voix de mastering pour les sons de jeu, sauf qu’elle spécifie AudioCategory_GameEffects pour le paramètre StreamCategory , qui est la valeur par défaut.
Création de l’effet de réverbération
Pour chaque voix, vous pouvez utiliser XAudio2 pour créer des séquences d’effets qui traitent l’audio. Une telle séquence est appelée chaîne d’effet. Utilisez des chaînes d’effets lorsque vous souhaitez appliquer un ou plusieurs effets à une voix. Les chaînes d’effet peuvent être destructrices ; autrement dit, chaque effet de la chaîne peut remplacer la mémoire tampon audio. Cette propriété est importante, car XAudio2 ne garantit pas que les mémoires tampons de sortie sont initialisées avec le silence. Les objets d’effet sont représentés dans XAudio2 par des objets de traitement audio multiplateforme (XAPO). Pour plus d’informations sur XAPO, consultez Vue d’ensemble de XAPO.
Lorsque vous créez une chaîne d’effets, procédez comme suit :
Créez l’objet d’effet.
Remplissez une structure XAUDIO2_EFFECT_DESCRIPTOR avec des données d’effet.
Remplissez une structure XAUDIO2_EFFECT_CHAIN avec des données.
Appliquez la chaîne d’effet à une voix.
Remplissez une structure de paramètres d’effet et appliquez-la à l’effet.
Désactivez ou activez l’effet chaque fois que cela est approprié.
La classe Audio définit la méthode CreateReverb pour créer la chaîne d’effets qui implémente la réverbération. Cette méthode appelle la méthode XAudio2CreateReverb pour créer un objet ComPtr<IUnknown>, soundEffectXAPO, qui agit comme la voix de sous-mélange pour l’effet de réverbération.
Microsoft::WRL::ComPtr<IUnknown> soundEffectXAPO;
DX::ThrowIfFailed(
XAudio2CreateReverb(&soundEffectXAPO)
);
La structure XAUDIO2_EFFECT_DESCRIPTOR contient des informations sur un XAPO à utiliser dans une chaîne d’effets, par exemple le nombre cible de canaux de sortie. La méthode Audio ::CreateReverb crée un objet XAUDIO2_EFFECT_DESCRIPTOR , soundEffectdescriptor, défini sur l’état désactivé, utilise deux canaux de sortie et fait référence à soundEffectXAPO pour l’effet de réverbération. soundEffectdescriptor démarre dans l’état désactivé, car le jeu doit définir des paramètres avant que l’effet commence à modifier les sons du jeu. Marble Maze utilise deux canaux de sortie pour simplifier la logique de l’effet de réverbération.
soundEffectdescriptor.InitialState = false;
soundEffectdescriptor.OutputChannels = 2;
soundEffectdescriptor.pEffect = soundEffectXAPO.Get();
Si votre chaîne d’effets a plusieurs effets, chaque effet nécessite un objet. La structure XAUDIO2_EFFECT_CHAIN contient le tableau d’objets XAUDIO2_EFFECT_DESCRIPTOR qui participent à l’effet. L’exemple suivant montre comment la méthode Audio ::CreateReverb spécifie l’effet unique pour implémenter la réverbération.
XAUDIO2_EFFECT_CHAIN soundEffectChain;
// ...
soundEffectChain.EffectCount = 1;
soundEffectChain.pEffectDescriptors = &soundEffectdescriptor;
La méthode Audio ::CreateReverb appelle la méthode IXAudio2 ::CreateSubmixVoice pour créer la voix de sous-mélange pour l’effet. Il spécifie l’objet XAUDIO2_EFFECT_CHAIN , soundEffectChain, pour le paramètre pEffectChain afin d’associer la chaîne d’effet à la voix. Marble Maze spécifie également deux canaux de sortie et un taux d’échantillonnage de 48 kilohertz.
DX::ThrowIfFailed(
engine->CreateSubmixVoice(newSubmix, 2, 48000, 0, 0, nullptr, &soundEffectChain)
);
Conseil
Si vous souhaitez attacher une chaîne d’effets existante à une voix de sous-mélange existante ou remplacer la chaîne d’effet actuelle, utilisez la méthode IXAudio2Voice ::SetEffectChain .
La méthode Audio ::CreateReverb appelle IXAudio2Voice ::SetEffectParameters pour définir des paramètres supplémentaires associés à l’effet. Cette méthode prend une structure de paramètres spécifique à l’effet. Un objet XAUDIO2FX_REVERB_PARAMETERS , m_reverbParametersSmall, qui contient les paramètres d’effet de réverbération, est initialisé dans la méthode Audio ::Initialize , car chaque effet de réverbération partage les mêmes paramètres. L’exemple suivant montre comment la méthode Audio ::Initialize initialise les paramètres de réverbération pour la réverbération de champ proche.
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;
Cet exemple utilise les valeurs par défaut pour la plupart des paramètres de réverbération, mais il définit DisableLateField sur TRUE pour spécifier la réverbération de champ proche, EarlyDiffusion sur 4 pour simuler des surfaces proches plates et LateDiffusion sur 15 pour simuler des surfaces distantes très diffuses. Les surfaces proches plates provoquent des échos pour vous atteindre plus rapidement et plus fort ; les surfaces éloignées diffuses provoquent des échos plus calmes et vous atteignent plus lentement. Vous pouvez expérimenter des valeurs de réverbération pour obtenir l’effet souhaité dans votre jeu ou utiliser la méthode ReverbConvertI3DL2ToNative pour utiliser les paramètres I3DL2 standard (Interactive 3D Audio Rendering Guidelines Level 2.0).
L’exemple suivant montre comment Audio ::CreateReverb définit les paramètres de réverbération. newSubmix est un objet IXAudio2SubmixVoice**. les paramètres sont un objet XAUDIO2FX_REVERB_PARAMETERS*.
DX::ThrowIfFailed(
(*newSubmix)->SetEffectParameters(0, parameters, sizeof(m_reverbParametersSmall))
);
La méthode Audio ::CreateReverb se termine en activant l’effet à l’aide d’IXAudio2Voice ::EnableEffect si l’indicateur enableEffect est défini. Il définit également son volume à l’aide d’IXAudio2Voice ::SetVolume et de la matrice de sortie à l’aide d’IXAudio2Voice ::SetOutputMatrix. Cette partie définit le volume sur plein (1,0), puis spécifie que la matrice de volume doit être silencieuse pour les entrées gauche et droite et les haut-parleurs de sortie gauche et droite. Nous le faisons parce que d’autres codes plus tard s’évanouissent entre les deux réverbérations (simulant la transition d’être près d’un mur à l’être dans une grande pièce), ou désactive les deux réverbérations si nécessaire. Lorsque le chemin de réverbération est ultérieurement non activé, le jeu définit une matrice de {1.0f, 0.0f, 0.0f, 1.0f} pour acheminer la sortie de réverbération gauche vers l’entrée gauche de la voix mastering et la sortie de réverbération droite vers l’entrée droite de la voix de mastering.
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 appelle la méthode Audio ::CreateReverb quatre fois : deux fois pour la musique d’arrière-plan et deux fois pour les sons de jeu. L’exemple suivant montre comment Marble Maze appelle la méthode CreateReverb pour la musique d’arrière-plan.
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersSmall,
&m_musicReverbVoiceSmallRoom,
true
);
CreateReverb(
m_musicEngine,
m_musicMasteringVoice,
&m_reverbParametersLarge,
&m_musicReverbVoiceLargeRoom,
true
);
Pour obtenir la liste des sources possibles d’effets à utiliser avec XAudio2, consultez effets audio XAudio2.
Chargement de données audio à partir d’un fichier
Marble Maze définit la classe MediaStreamer , qui utilise Media Foundation pour charger des ressources audio à partir de fichiers. Marble Maze utilise un objet MediaStreamer pour charger chaque fichier audio.
Marble Maze appelle la méthode MediaStreamer ::Initialize pour initialiser chaque flux audio. Voici comment la méthode Audio ::CreateResources appelle MediaStreamer ::Initialize pour initialiser le flux audio pour la musique d’arrière-plan :
// 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");
La méthode MediaStreamer ::Initialize commence par appeler la méthode MFStartup pour initialiser Media Foundation. MF_VERSION est une macro définie dans mfapi.h, et c’est ce que nous spécifions comme version de Media Foundation à utiliser.
DX::ThrowIfFailed(
MFStartup(MF_VERSION)
);
MediaStreamer ::Initialize appelle ensuite MFCreateSourceReaderFromURL pour créer un objet IMFSourceReader . Un objet IMFSourceReader , m_reader, lit les données multimédias du fichier spécifié par URL.
DX::ThrowIfFailed(
MFCreateSourceReaderFromURL(url, nullptr, &m_reader)
);
La méthode MediaStreamer ::Initialize crée ensuite un objet IMFMediaType à l’aide de MFCreateMediaType pour décrire le format du flux audio. Un format audio a deux types : un type principal et un sous-type. Le type principal définit le format global du média, tel que la vidéo, l’audio, le script, etc. Le sous-type définit le format, tel que PCM, ADPCM ou WMA.
La méthode MediaStreamer ::Initialize utilise la méthode IMFAttributes ::SetGUID pour spécifier le type principal (MF_MT_MAJOR_TYPE) en tant qu’audio (MFMediaType_Audio) et le type secondaire (MF_MT_SUBTYPE) comme audio PCM non compressé (MFAudioFormat_PCM). MF_MT_MAJOR_TYPE et MF_MT_SUBTYPE sont des attributs Media Foundation. MFMediaType_Audio et MFAudioFormat_PCM sont des GUID de type et de sous-type . Pour plus d’informations, consultez types de supports audio. La méthode IMFSourceReader ::SetCurrentMediaType associe le type de média au lecteur de flux.
// 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())
);
La méthode MediaStreamer ::Initialize obtient ensuite le format de média de sortie complet de Media Foundation à l’aide de IMFSourceReader ::GetCurrentMediaType et appelle la méthode MFCreateWaveFormatExFromMFMediaType pour convertir le type de média audio Media Foundation en structure WAVEFORMATEX . La structure WAVEFORMATEX définit le format des données waveform-audio. Marble Maze utilise cette structure pour créer les voix sources et appliquer le filtre de passe basse au son roulant de marbre.
// 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);
Important
La méthode MFCreateWaveFormatExFromMFMediaType utilise CoTaskMemAlloc pour allouer l’objet WAVEFORMATEX . Par conséquent, assurez-vous d’appeler CoTaskMemFree lorsque vous avez terminé d’utiliser cet objet.
La méthode MediaStreamer ::Initialize se termine en calculant la longueur du flux, m_maxStreamLengthInBytes, en octets. Pour ce faire, il appelle la méthode IMFSourceReader ::GetPresentationAttribute pour obtenir la durée du flux audio en unités de 100 nanosecondes, convertit la durée en sections, puis multiplie par le taux de transfert de données moyen en octets par seconde. Marble Maze utilise ultérieurement cette valeur pour allouer la mémoire tampon qui contient chaque son de jeu.
// 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
);
Création des voix sources
Marble Maze crée des voix sources XAudio2 pour jouer à chacun de ses sons de jeu et musique dans les voix sources. La classe Audio définit un objet IXAudio2SourceVoice pour la musique d’arrière-plan et un tableau d’objets SoundEffectData pour contenir les sons de jeu. La structure SoundEffectData contient l’objet IXAudio2SourceVoice pour un effet et définit également d’autres données liées à l’effet, telles que la mémoire tampon audio. Audio.h définit l’énumération SoundEvent . Marble Maze utilise cette énumération pour identifier chaque son de jeu. La classe Audio utilise également cette énumération pour indexer le tableau d’objets SoundEffectData .
enum SoundEvent
{
RollingEvent = 0,
FallingEvent = 1,
CollisionEvent = 2,
CheckpointEvent = 3,
MenuChangeEvent = 4,
MenuSelectedEvent = 5,
LastSoundEvent,
};
Le tableau suivant montre la relation entre chacune de ces valeurs, le fichier qui contient les données sonores associées et une brève description de ce que chaque son représente. Les fichiers audio se trouvent dans le dossier \Media\Audio .
Valeur SoundEvent | Nom de fichier | Description |
---|---|---|
RollingEvent | MarbleRoll.wav | Joué comme rouleaux de marbre. |
FallingEvent | MarbleFall.wav | Joué quand la bille tombe au labyrinthe. |
CollisionEvent | MarbleHit.wav | Joué quand la marbre entre en collision avec le labyrinthe. |
CheckpointEvent | Checkpoint.wav | Joué lorsque la bille passe sur un point de contrôle. |
MenuChangeEvent | MenuChange.wav | Lu lorsque l’utilisateur modifie l’élément de menu actif. |
MenuSelectedEvent | MenuSelect.wav | Lu lorsque l’utilisateur sélectionne un élément de menu. |
L’exemple suivant montre comment la méthode Audio ::CreateResources crée la voix source pour la musique d’arrière-plan. La structure XAUDIO2_SEND_DESCRIPTOR définit la voix de destination cible d’une autre voix et spécifie si un filtre doit être utilisé. Marble Maze appelle la méthode Audio ::SetSoundEffectFilter pour utiliser les filtres pour modifier le son de la balle lors de son rouleau. La structure XAUDIO2_VOICE_SENDS définit l’ensemble de voix pour recevoir des données d’une voix de sortie unique. Marble Maze envoie des données de la voix source à la voix de mastering (pour la partie sèche, ou non, d’un son de lecture) et aux deux voix de sous-mélange qui implémentent la partie humide ou réverbération d’un son de lecture.
La méthode IXAudio2 ::CreateSourceVoice crée et configure une voix source. Il prend une structure WAVEFORMATEX qui définit le format des mémoires tampons audio envoyées à la voix. Comme mentionné précédemment, Marble Maze utilise le format 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)
);
Lecture de musique d’arrière-plan
Une voix source est créée à l’état arrêté. Marble Maze démarre la musique d’arrière-plan dans la boucle de jeu. Le premier appel à MarbleMazeMain ::Update appelle Audio ::Start pour démarrer la musique d’arrière-plan.
if (!m_audio.m_isAudioStarted)
{
m_audio.Start();
}
La méthode Audio ::Start appelle IXAudio2SourceVoice ::Start pour commencer à traiter la voix source pour la musique d’arrière-plan.
void Audio::Start()
{
if (m_engineExperiencedCriticalError)
{
return;
}
HRESULT hr = m_musicSourceVoice->Start(0);
if SUCCEEDED(hr) {
m_isAudioStarted = true;
}
else
{
m_engineExperiencedCriticalError = true;
}
}
La voix source transmet ces données audio à l’étape suivante du graphe audio. Dans le cas de Marble Maze, la prochaine étape contient deux voix de sous-mélange qui appliquent les deux effets de réverbération à l’audio. Une voix submixée applique une réverbération de champ tardif proche ; la seconde applique une réverbération de champ lointain.
La quantité que chaque voix submixée contribue à la combinaison finale est déterminée par la taille et la forme de la pièce. La réverbération de champ proche contribue plus lorsque la balle est près d’un mur ou dans une petite pièce, et la réverbération de champ tardif contribue plus lorsque la balle est dans un grand espace. Cette technique produit un effet d’écho plus réaliste au fur et à mesure que le marbre se déplace dans le labyrinthe. Pour en savoir plus sur la façon dont Marble Maze implémente cet effet, consultez Audio ::SetRoomSize et Physics ::CalculateCurrentRoomSize dans le code source Marble Maze.
Remarque
Dans un jeu dans lequel la plupart des tailles de salle sont relativement identiques, vous pouvez utiliser un modèle de réverbération plus simple. Par exemple, vous pouvez utiliser un paramètre de réverbération pour toutes les salles ou créer un paramètre de réverbération prédéfini pour chaque salle.
La méthode Audio ::CreateResources utilise Media Foundation pour charger la musique d’arrière-plan. À ce stade, toutefois, la voix source n’a pas de données audio à utiliser. En outre, étant donné que les boucles de musique d’arrière-plan, la voix source doit être régulièrement mise à jour avec des données afin que la musique continue de jouer.
Pour conserver la voix source remplie de données, la boucle de jeu met à jour les mémoires tampons audio chaque image. La méthode MarbleMazeMain ::Render appelle Audio ::Render pour traiter la mémoire tampon audio de musique d’arrière-plan. La classe Audio définit un tableau de trois mémoires tampons audio, m_audioBuffers. Chaque mémoire tampon contient 64 Ko (65536 octets) de données. La boucle lit les données de l’objet Media Foundation et écrit ces données dans la voix source jusqu’à ce que la voix source ait trois mémoires tampons mises en file d’attente.
Attention
Bien que Marble Maze utilise une mémoire tampon de 64 Ko pour stocker des données musicales, vous devrez peut-être utiliser une mémoire tampon plus grande ou plus petite. Ce montant dépend des exigences de votre jeu.
// 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;
}
}
La boucle gère également lorsque l’objet Media Foundation atteint la fin du flux. Dans ce cas, il appelle la méthode IMFSourceReader ::SetCurrentPosition pour réinitialiser la position de la source audio.
void MediaStreamer::Restart()
{
if (m_reader == nullptr)
{
return;
}
PROPVARIANT var = {0};
var.vt = VT_I8;
DX::ThrowIfFailed(
m_reader->SetCurrentPosition(GUID_NULL, var)
);
}
Pour implémenter une boucle audio pour une mémoire tampon unique (ou pour un son entier entièrement chargé en mémoire), vous pouvez définir le champ XAUDIO2_BUFFER ::LoopCount sur XAUDIO2_LOOP_INFINITE lorsque vous initialisez le son. Marble Maze utilise cette technique pour jouer le son propagé pour la marbre.
if (sound == RollingEvent)
{
m_soundEffects[sound].m_audioBuffer.LoopCount = XAUDIO2_LOOP_INFINITE;
}
Toutefois, pour la musique d’arrière-plan, Marble Maze gère directement les mémoires tampons afin qu’elle puisse mieux contrôler la quantité de mémoire utilisée. Lorsque vos fichiers de musique sont volumineux, vous pouvez diffuser en continu les données musicales dans des mémoires tampons plus petites. Cela peut aider à équilibrer la taille de la mémoire avec la fréquence de la capacité du jeu à traiter et à diffuser en continu des données audio.
Conseil
Si votre jeu a une fréquence d’images faible ou variable, le traitement audio sur le thread principal peut produire des pauses ou des fenêtres contextuelles inattendues dans l’audio, car le moteur audio ne dispose pas de données audio mises en mémoire tampon insuffisantes pour fonctionner. Si votre jeu est sensible à ce problème, envisagez de traiter l’audio sur un thread distinct qui n’effectue pas de rendu. Cette approche est particulièrement utile sur les ordinateurs qui ont plusieurs processeurs, car votre jeu peut utiliser des processeurs inactifs.
Réaction aux événements de jeu
La classe Audio fournit des méthodes telles que PlaySoundEffect, IsSoundEffectStarted, StopSoundEffectVolume, SetSoundEffectVolume, SetSoundEffectPitch et SetSoundEffectFilter pour permettre au jeu de contrôler quand les sons sont lus et arrêtés, et de contrôler les propriétés sonores telles que le volume et la hauteur. Par exemple, si la bille tombe hors du labyrinthe, MarbleMazeMain ::Update appelle la méthode Audio ::P laySoundEffect pour lire le son de FallingEvent .
m_audio.PlaySoundEffect(FallingEvent);
La méthode Audio ::P laySoundEffect appelle la méthode IXAudio2SourceVoice ::Start pour commencer la lecture du son. Si la méthode IXAudio2SourceVoice ::Start a déjà été appelée, elle n’est pas redémarrer. Audio ::P laySoundEffect effectue ensuite une logique personnalisée pour certains sons.
void Audio::PlaySoundEffect(SoundEvent sound)
{
XAUDIO2_BUFFER buf = {0};
XAUDIO2_VOICE_STATE state = {0};
if (m_engineExperiencedCriticalError)
{
// If there's an error, then we'll recreate the engine on the next
// render pass.
return;
}
SoundEffectData* soundEffect = &m_soundEffects[sound];
HRESULT hr = soundEffect->m_soundEffectSourceVoice->Start();
if FAILED(hr)
{
m_engineExperiencedCriticalError = true;
return;
}
// For one-off voices, submit a new buffer if there's none queued up,
// and allow up to two collisions to be queued up.
if (sound != RollingEvent)
{
XAUDIO2_VOICE_STATE state = {0};
soundEffect->m_soundEffectSourceVoice->
GetState(&state, XAUDIO2_VOICE_NOSAMPLESPLAYED);
if (state.BuffersQueued == 0)
{
soundEffect->m_soundEffectSourceVoice->
SubmitSourceBuffer(&soundEffect->m_audioBuffer);
}
else if (state.BuffersQueued < 2 && sound == CollisionEvent)
{
soundEffect->m_soundEffectSourceVoice->
SubmitSourceBuffer(&soundEffect->m_audioBuffer);
}
// For the menu clicks, we want to stop the voice and replay the click
// right away.
// Note that stopping and then flushing could cause a glitch due to the
// waveform not being at a zero-crossing, but due to the nature of the
// sound (fast and 'clicky'), we don't mind.
if (state.BuffersQueued > 0 && sound == MenuChangeEvent)
{
soundEffect->m_soundEffectSourceVoice->Stop();
soundEffect->m_soundEffectSourceVoice->FlushSourceBuffers();
soundEffect->m_soundEffectSourceVoice->
SubmitSourceBuffer(&soundEffect->m_audioBuffer);
soundEffect->m_soundEffectSourceVoice->Start();
}
}
m_soundEffects[sound].m_soundEffectStarted = true;
}
Pour les sons autres que propagés, la méthode Audio ::P laySoundEffect appelle IXAudio2SourceVoice ::GetState pour déterminer le nombre de mémoires tampons lues par la voix source. Il appelle IXAudio2SourceVoice ::SubmitSourceBuffer pour ajouter les données audio du son à la file d’attente d’entrée de la voix si aucune mémoire tampon n’est active. La méthode Audio ::P laySoundEffect permet également de lire deux fois le son de collision en séquence. Cela se produit, par exemple, lorsque la bille entre en collision avec un coin du labyrinthe.
Comme décrit déjà, la classe Audio utilise l’indicateur XAUDIO2_LOOP_INFINITE lorsqu’il initialise le son pour l’événement propagé. Le son démarre la lecture en boucle la première fois que Audio ::P laySoundEffect est appelé pour cet événement. Pour simplifier la logique de lecture du son propagé, Marble Maze mute le son au lieu de l’arrêter. Lorsque la bille change de vitesse, Marble Maze modifie la hauteur et le volume du son pour lui donner un effet plus réaliste. L’exemple suivant montre comment la méthode MarbleMazeMain ::Update met à jour la hauteur et le volume de la bille à mesure que sa vitesse change et comment elle mute le son en définissant son volume sur zéro lorsque la bille s’arrête.
// 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);
}
Réaction aux événements de suspension et de reprise
La structure de l’application Marble Maze décrit comment Marble Maze prend en charge la suspension et la reprise. Lorsque le jeu est suspendu, le jeu suspend l’audio. Lorsque le jeu reprend, le jeu reprend l’audio là où il s’est arrêté. Nous procédons ainsi pour suivre la meilleure pratique de ne pas utiliser de ressources lorsque vous savez qu’elles ne sont pas nécessaires.
La méthode Audio ::SuspendAudio est appelée lorsque le jeu est suspendu. Cette méthode appelle la méthode IXAudio2 ::StopEngine pour arrêter tout l’audio. Bien que IXAudio2 ::StopEngine arrête immédiatement toute sortie audio, il conserve le graphique audio et ses paramètres d’effet (par exemple, l’effet de réverbération appliqué lorsque la bille rebondit).
// 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;
}
La méthode Audio ::ResumeAudio est appelée lorsque le jeu est repris. Cette méthode utilise la méthode IXAudio2 ::StartEngine pour redémarrer l’audio. Étant donné que l’appel à IXAudio2 ::StopEngine conserve le graphique audio et ses paramètres d’effet, la sortie audio reprend là où elle s’est arrêtée.
// 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;
}
}
Gestion des changements de casque et d’appareil
Marble Maze utilise des rappels de moteur pour gérer les défaillances du moteur XAudio2, par exemple lorsque l’appareil audio change. Une cause probable d’un changement d’appareil est lorsque l’utilisateur du jeu se connecte ou déconnecte le casque. Nous vous recommandons d’implémenter le rappel du moteur qui gère les modifications apportées aux appareils. Sinon, votre jeu cesse de lire le son lorsque l’utilisateur se connecte ou supprime des casques, jusqu’à ce que le jeu soit redémarré.
Audio.h définit la classe AudioEngineCallbacks . Cette classe implémente l’interface IXAudio2EngineCallback .
class AudioEngineCallbacks: public IXAudio2EngineCallback
{
private:
Audio* m_audio;
public :
AudioEngineCallbacks(){};
void Initialize(Audio* audio);
// Called by XAudio2 just before an audio processing pass begins.
void _stdcall OnProcessingPassStart(){};
// Called just after an audio processing pass ends.
void _stdcall OnProcessingPassEnd(){};
// Called when a critical system error causes XAudio2
// to be closed and restarted. The error code is given in Error.
void _stdcall OnCriticalError(HRESULT Error);
};
L’interface IXAudio2EngineCallback permet à votre code d’être averti lorsque des événements de traitement audio se produisent et lorsque le moteur rencontre une erreur critique. Pour vous inscrire aux rappels, Marble Maze appelle la méthode IXAudio2 ::RegisterForCallbacks dans Audio ::CreateResources, après avoir créé l’objet IXAudio2 pour le moteur de musique.
m_musicEngineCallback.Initialize(this);
m_musicEngine->RegisterForCallbacks(&m_musicEngineCallback);
Marble Maze ne nécessite pas de notification lorsque le traitement audio démarre ou se termine. Par conséquent, il implémente les méthodes IXAudio2EngineCallback ::OnProcessingPassStart et IXAudio2EngineCallback ::OnProcessingPassEnd pour ne rien faire. Pour la méthode IXAudio2EngineCallback ::OnCriticalError , Marble Maze appelle la méthode SetEngineExperiencedCriticalError , qui définit l’indicateur 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;
}
Lorsqu’une erreur critique se produit, le traitement audio s’arrête et tous les appels supplémentaires à XAudio2 échouent. Pour récupérer de cette situation, vous devez libérer l’instance XAudio2 et en créer une nouvelle. La méthode Audio ::Render , appelée à partir de la boucle de jeu, vérifie d’abord l’indicateur de m_engineExperiencedCriticalError . Si cet indicateur est défini, il efface l’indicateur, libère l’instance XAudio2 actuelle, initialise les ressources, puis démarre la musique d’arrière-plan.
if (m_engineExperiencedCriticalError)
{
m_engineExperiencedCriticalError = false;
ReleaseResources();
Initialize();
CreateResources();
Start();
if (m_engineExperiencedCriticalError)
{
return;
}
}
Marble Maze utilise également l’indicateur de m_engineExperiencedCriticalError pour se protéger contre l’appel à XAudio2 lorsqu’aucun appareil audio n’est disponible. Par exemple, la méthode MarbleMazeMain ::Update ne traite pas l’audio pour les événements de déploiement ou de collision lorsque cet indicateur est défini. L’application tente de réparer le moteur audio chaque image si nécessaire ; Toutefois, l’indicateur de m_engineExperiencedCriticalError peut toujours être défini si l’ordinateur n’a pas d’appareil audio ou si les casques sont déconnectés et qu’il n’existe aucun autre périphérique audio disponible.
Attention
En règle générale, n’effectuez pas d’opérations bloquantes dans le corps d’un rappel de moteur. Cela peut entraîner des problèmes de performances. Marble Maze définit un indicateur dans le rappel OnCriticalError et gère ultérieurement l’erreur pendant la phase de traitement audio standard. Pour plus d’informations sur les rappels XAudio2, consultez Rappels XAudio2.
Conclusion
Cela encapsule l’exemple de jeu Marble Maze ! Bien qu’il s’agit d’un jeu relativement simple, il contient de nombreuses parties importantes qui vont dans n’importe quel jeu DirectX UWP, et est un bon exemple à suivre lors de la création de votre propre jeu.
Maintenant que vous avez terminé le suivi, essayez de vous familiariser avec le code source et de voir ce qui se passe. Ou consultez Créer un jeu UWP simple avec DirectX, un autre exemple de jeu UWP DirectX.
Prêt à aller plus loin avec DirectX ? Consultez ensuite nos guides de programmation DirectX.
Si vous êtes intéressé par le développement de jeux sur UWP en général, consultez la documentation sur la programmation de jeux.