mirror of https://github.com/axmolengine/axmol.git
577 lines
15 KiB
C++
577 lines
15 KiB
C++
//// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
|
|
//// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
|
|
//// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
|
|
//// PARTICULAR PURPOSE.
|
|
////
|
|
//// Copyright (c) Microsoft Corporation. All rights reserved
|
|
|
|
#include "pch.h"
|
|
#include "Audio.h"
|
|
#include "MediaStreamer.h"
|
|
|
|
static std::wstring CCUtf8ToUnicode(const char * pszUtf8Str, unsigned len/* = -1*/)
|
|
{
|
|
std::wstring ret;
|
|
do
|
|
{
|
|
if (! pszUtf8Str) break;
|
|
// get UTF8 string length
|
|
if (-1 == len)
|
|
{
|
|
len = strlen(pszUtf8Str);
|
|
}
|
|
if (len <= 0) break;
|
|
|
|
// get UTF16 string length
|
|
int wLen = MultiByteToWideChar(CP_UTF8, 0, pszUtf8Str, len, 0, 0);
|
|
if (0 == wLen || 0xFFFD == wLen) break;
|
|
|
|
// convert string
|
|
wchar_t * pwszStr = new wchar_t[wLen + 1];
|
|
if (! pwszStr) break;
|
|
pwszStr[wLen] = 0;
|
|
MultiByteToWideChar(CP_UTF8, 0, pszUtf8Str, len, pwszStr, wLen + 1);
|
|
ret = pwszStr;
|
|
if(pwszStr) { delete[] (pwszStr);};
|
|
} while (0);
|
|
return ret;
|
|
}
|
|
|
|
static inline void ThrowIfFailed(HRESULT hr)
|
|
{
|
|
if (FAILED(hr))
|
|
{
|
|
// Set a breakpoint on this line to catch DirectX API errors
|
|
throw Platform::Exception::CreateException(hr);
|
|
}
|
|
}
|
|
|
|
void AudioEngineCallbacks::Initialize(Audio *audio)
|
|
{
|
|
m_audio = audio;
|
|
}
|
|
|
|
// Called in the event of a critical system error which requires XAudio2
|
|
// to be closed down and restarted. The error code is given in error.
|
|
void _stdcall AudioEngineCallbacks::OnCriticalError(HRESULT Error)
|
|
{
|
|
m_audio->SetEngineExperiencedCriticalError();
|
|
};
|
|
|
|
Audio::Audio() :
|
|
m_backgroundID(0),
|
|
m_soundEffctVolume(1.0),
|
|
m_backgroundMusicVolume(1.0f)
|
|
|
|
{
|
|
}
|
|
|
|
void Audio::Initialize()
|
|
{
|
|
m_engineExperiencedCriticalError = false;
|
|
|
|
m_musicEngine = nullptr;
|
|
m_soundEffectEngine = nullptr;
|
|
m_musicMasteringVoice = nullptr;
|
|
m_soundEffectMasteringVoice = nullptr;
|
|
}
|
|
|
|
void Audio::CreateResources()
|
|
{
|
|
try
|
|
{
|
|
ThrowIfFailed(
|
|
XAudio2Create(&m_musicEngine)
|
|
);
|
|
|
|
#if defined(_DEBUG)
|
|
XAUDIO2_DEBUG_CONFIGURATION debugConfig = {0};
|
|
debugConfig.BreakMask = XAUDIO2_LOG_ERRORS;
|
|
debugConfig.TraceMask = XAUDIO2_LOG_ERRORS;
|
|
m_musicEngine->SetDebugConfiguration(&debugConfig);
|
|
#endif
|
|
|
|
m_musicEngineCallback.Initialize(this);
|
|
m_musicEngine->RegisterForCallbacks(&m_musicEngineCallback);
|
|
|
|
// 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.
|
|
ThrowIfFailed(
|
|
m_musicEngine->CreateMasteringVoice(&m_musicMasteringVoice, XAUDIO2_DEFAULT_CHANNELS, XAUDIO2_DEFAULT_SAMPLERATE, 0, nullptr, nullptr, AudioCategory_GameMedia)
|
|
);
|
|
|
|
// Create a separate engine and mastering voice for sound effects in the sample
|
|
// Games will use many voices in a complex graph for audio, mixing all effects down to a
|
|
// single mastering voice.
|
|
// We are creating an entirely new engine instance and mastering voice in order to tag
|
|
// our sound effects with the audio category AudioCategory_GameEffects.
|
|
ThrowIfFailed(
|
|
XAudio2Create(&m_soundEffectEngine)
|
|
);
|
|
|
|
m_soundEffectEngineCallback.Initialize(this);
|
|
m_soundEffectEngine->RegisterForCallbacks(&m_soundEffectEngineCallback);
|
|
|
|
// We default the mastering voice to 2 channels to simplify the reverb logic.
|
|
ThrowIfFailed(
|
|
m_soundEffectEngine->CreateMasteringVoice(&m_soundEffectMasteringVoice, XAUDIO2_DEFAULT_CHANNELS, XAUDIO2_DEFAULT_SAMPLERATE, 0, nullptr, nullptr, AudioCategory_GameEffects)
|
|
);
|
|
}
|
|
catch (...)
|
|
{
|
|
m_engineExperiencedCriticalError = true;
|
|
}
|
|
}
|
|
|
|
unsigned int Audio::Hash(const char *key)
|
|
{
|
|
unsigned int len = strlen(key);
|
|
const char *end=key+len;
|
|
unsigned int hash;
|
|
|
|
for (hash = 0; key < end; key++)
|
|
{
|
|
hash *= 16777619;
|
|
hash ^= (unsigned int) (unsigned char) toupper(*key);
|
|
}
|
|
return (hash);
|
|
}
|
|
|
|
void Audio::ReleaseResources()
|
|
{
|
|
if (m_musicMasteringVoice != nullptr)
|
|
{
|
|
m_musicMasteringVoice->DestroyVoice();
|
|
m_musicMasteringVoice = nullptr;
|
|
}
|
|
if (m_soundEffectMasteringVoice != nullptr)
|
|
{
|
|
m_soundEffectMasteringVoice->DestroyVoice();
|
|
m_soundEffectMasteringVoice = nullptr;
|
|
}
|
|
|
|
EffectList::iterator EffectIter = m_soundEffects.begin();
|
|
for (; EffectIter != m_soundEffects.end(); EffectIter++)
|
|
{
|
|
if (EffectIter->second.m_soundEffectSourceVoice != nullptr)
|
|
{
|
|
EffectIter->second.m_soundEffectSourceVoice->DestroyVoice();
|
|
EffectIter->second.m_soundEffectSourceVoice = nullptr;
|
|
}
|
|
}
|
|
m_soundEffects.clear();
|
|
|
|
m_musicEngine = nullptr;
|
|
m_soundEffectEngine = nullptr;
|
|
}
|
|
|
|
void Audio::Start()
|
|
{
|
|
if (m_engineExperiencedCriticalError)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (! m_backgroundFile.empty())
|
|
PlayBackgroundMusic(m_backgroundFile.c_str(), m_backgroundLoop);
|
|
}
|
|
|
|
// 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)
|
|
{
|
|
ReleaseResources();
|
|
Initialize();
|
|
CreateResources();
|
|
Start();
|
|
if (m_engineExperiencedCriticalError)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Audio::PlayBackgroundMusic(const char* pszFilePath, bool bLoop)
|
|
{
|
|
m_backgroundFile = pszFilePath;
|
|
m_backgroundLoop = bLoop;
|
|
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
StopBackgroundMusic(true);
|
|
PlaySoundEffect(pszFilePath, bLoop, m_backgroundID, true);
|
|
}
|
|
|
|
void Audio::StopBackgroundMusic(bool bReleaseData)
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
StopSoundEffect(m_backgroundID);
|
|
|
|
if (bReleaseData)
|
|
UnloadSoundEffect(m_backgroundID);
|
|
}
|
|
|
|
void Audio::PauseBackgroundMusic()
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
PauseSoundEffect(m_backgroundID);
|
|
}
|
|
|
|
void Audio::ResumeBackgroundMusic()
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
ResumeSoundEffect(m_backgroundID);
|
|
}
|
|
|
|
void Audio::RewindBackgroundMusic()
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
RewindSoundEffect(m_backgroundID);
|
|
}
|
|
|
|
bool Audio::IsBackgroundMusicPlaying()
|
|
{
|
|
return IsSoundEffectStarted(m_backgroundID);
|
|
}
|
|
|
|
void Audio::SetBackgroundVolume(float volume)
|
|
{
|
|
m_backgroundMusicVolume = volume;
|
|
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
if (m_soundEffects.end() != m_soundEffects.find(m_backgroundID))
|
|
{
|
|
m_soundEffects[m_backgroundID].m_soundEffectSourceVoice->SetVolume(volume);
|
|
}
|
|
}
|
|
|
|
float Audio::GetBackgroundVolume()
|
|
{
|
|
return m_backgroundMusicVolume;
|
|
}
|
|
|
|
void Audio::SetSoundEffectVolume(float volume)
|
|
{
|
|
m_soundEffctVolume = volume;
|
|
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
EffectList::iterator iter;
|
|
for (iter = m_soundEffects.begin(); iter != m_soundEffects.end(); iter++)
|
|
{
|
|
if (iter->first != m_backgroundID)
|
|
iter->second.m_soundEffectSourceVoice->SetVolume(m_soundEffctVolume);
|
|
}
|
|
}
|
|
|
|
float Audio::GetSoundEffectVolume()
|
|
{
|
|
return m_soundEffctVolume;
|
|
}
|
|
|
|
void Audio::PlaySoundEffect(const char* pszFilePath, bool bLoop, unsigned int& sound, bool isMusic)
|
|
{
|
|
sound = Hash(pszFilePath);
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
{
|
|
PreloadSoundEffect(pszFilePath, isMusic);
|
|
}
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return;
|
|
|
|
m_soundEffects[sound].m_audioBuffer.LoopCount = bLoop ? XAUDIO2_LOOP_INFINITE : 0;
|
|
|
|
PlaySoundEffect(sound);
|
|
}
|
|
|
|
void Audio::PlaySoundEffect(unsigned int sound)
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return;
|
|
|
|
StopSoundEffect(sound);
|
|
|
|
ThrowIfFailed(
|
|
m_soundEffects[sound].m_soundEffectSourceVoice->SubmitSourceBuffer(&m_soundEffects[sound].m_audioBuffer)
|
|
);
|
|
|
|
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;
|
|
}
|
|
|
|
m_soundEffects[sound].m_soundEffectStarted = true;
|
|
}
|
|
|
|
void Audio::StopSoundEffect(unsigned int sound)
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return;
|
|
|
|
HRESULT hr = m_soundEffects[sound].m_soundEffectSourceVoice->Stop();
|
|
HRESULT hr1 = m_soundEffects[sound].m_soundEffectSourceVoice->FlushSourceBuffers();
|
|
if (FAILED(hr) || FAILED(hr1))
|
|
{
|
|
// If there's an error, then we'll recreate the engine on the next render pass
|
|
m_engineExperiencedCriticalError = true;
|
|
return;
|
|
}
|
|
|
|
m_soundEffects[sound].m_soundEffectStarted = false;
|
|
}
|
|
|
|
void Audio::PauseSoundEffect(unsigned int sound)
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return;
|
|
|
|
HRESULT hr = m_soundEffects[sound].m_soundEffectSourceVoice->Stop();
|
|
if FAILED(hr)
|
|
{
|
|
// If there's an error, then we'll recreate the engine on the next render pass
|
|
m_engineExperiencedCriticalError = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Audio::ResumeSoundEffect(unsigned int sound)
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return;
|
|
|
|
HRESULT hr = m_soundEffects[sound].m_soundEffectSourceVoice->Start();
|
|
if FAILED(hr)
|
|
{
|
|
// If there's an error, then we'll recreate the engine on the next render pass
|
|
m_engineExperiencedCriticalError = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
void Audio::RewindSoundEffect(unsigned int sound)
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return;
|
|
|
|
StopSoundEffect(sound);
|
|
PlaySoundEffect(sound);
|
|
}
|
|
|
|
void Audio::PauseAllSoundEffects()
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
EffectList::iterator iter;
|
|
for (iter = m_soundEffects.begin(); iter != m_soundEffects.end(); iter++)
|
|
{
|
|
PauseSoundEffect(iter->first);
|
|
}
|
|
}
|
|
|
|
void Audio::ResumeAllSoundEffects()
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
EffectList::iterator iter;
|
|
for (iter = m_soundEffects.begin(); iter != m_soundEffects.end(); iter++)
|
|
{
|
|
ResumeSoundEffect(iter->first);
|
|
}
|
|
}
|
|
|
|
void Audio::StopAllSoundEffects()
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
EffectList::iterator iter;
|
|
for (iter = m_soundEffects.begin(); iter != m_soundEffects.end(); iter++)
|
|
{
|
|
StopSoundEffect(iter->first);
|
|
}
|
|
}
|
|
|
|
bool Audio::IsSoundEffectStarted(unsigned int sound)
|
|
{
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return false;
|
|
|
|
return m_soundEffects[sound].m_soundEffectStarted;
|
|
}
|
|
|
|
void Audio::PreloadSoundEffect(const char* pszFilePath, bool isMusic)
|
|
{
|
|
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
int sound = Hash(pszFilePath);
|
|
|
|
if (m_soundEffects.end() != m_soundEffects.find(sound))
|
|
{
|
|
return;
|
|
}
|
|
|
|
MediaStreamer mediaStreamer;
|
|
mediaStreamer.Initialize(CCUtf8ToUnicode(pszFilePath, -1).c_str());
|
|
m_soundEffects[sound].m_soundID = sound;
|
|
|
|
uint32 bufferLength = mediaStreamer.GetMaxStreamLengthInBytes();
|
|
|
|
if (m_soundEffects.find(sound) != m_soundEffects.end())
|
|
{
|
|
if (m_soundEffects[sound].m_soundEffectBufferData)
|
|
{
|
|
delete[] m_soundEffects[sound].m_soundEffectBufferData;
|
|
m_soundEffects[sound].m_soundEffectBufferData = NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_soundEffects[sound].m_soundEffectBufferData = NULL;
|
|
}
|
|
|
|
m_soundEffects[sound].m_soundEffectBufferData = new byte[bufferLength];
|
|
mediaStreamer.ReadAll(m_soundEffects[sound].m_soundEffectBufferData, bufferLength, &m_soundEffects[sound].m_soundEffectBufferLength);
|
|
|
|
if (isMusic)
|
|
{
|
|
XAUDIO2_SEND_DESCRIPTOR descriptors[1];
|
|
descriptors[0].pOutputVoice = m_musicMasteringVoice;
|
|
descriptors[0].Flags = 0;
|
|
XAUDIO2_VOICE_SENDS sends = {0};
|
|
sends.SendCount = 1;
|
|
sends.pSends = descriptors;
|
|
|
|
ThrowIfFailed(
|
|
m_musicEngine->CreateSourceVoice(&m_soundEffects[sound].m_soundEffectSourceVoice,
|
|
&(mediaStreamer.GetOutputWaveFormatEx()), 0, 1.0f, &m_voiceContext, &sends)
|
|
);
|
|
//fix bug: set a initial volume
|
|
m_soundEffects[sound].m_soundEffectSourceVoice->SetVolume(m_backgroundMusicVolume);
|
|
} else
|
|
{
|
|
XAUDIO2_SEND_DESCRIPTOR descriptors[1];
|
|
descriptors[0].pOutputVoice = m_soundEffectMasteringVoice;
|
|
descriptors[0].Flags = 0;
|
|
XAUDIO2_VOICE_SENDS sends = {0};
|
|
sends.SendCount = 1;
|
|
sends.pSends = descriptors;
|
|
|
|
ThrowIfFailed(
|
|
m_soundEffectEngine->CreateSourceVoice(&m_soundEffects[sound].m_soundEffectSourceVoice,
|
|
&(mediaStreamer.GetOutputWaveFormatEx()), 0, 1.0f, &m_voiceContext, &sends, nullptr)
|
|
);
|
|
//fix bug: set a initial volume
|
|
m_soundEffects[sound].m_soundEffectSourceVoice->SetVolume(m_soundEffctVolume);
|
|
}
|
|
|
|
m_soundEffects[sound].m_soundEffectSampleRate = mediaStreamer.GetOutputWaveFormatEx().nSamplesPerSec;
|
|
|
|
// Queue in-memory buffer for playback
|
|
ZeroMemory(&m_soundEffects[sound].m_audioBuffer, sizeof(m_soundEffects[sound].m_audioBuffer));
|
|
|
|
m_soundEffects[sound].m_audioBuffer.AudioBytes = m_soundEffects[sound].m_soundEffectBufferLength;
|
|
m_soundEffects[sound].m_audioBuffer.pAudioData = m_soundEffects[sound].m_soundEffectBufferData;
|
|
m_soundEffects[sound].m_audioBuffer.pContext = &m_soundEffects[sound];
|
|
m_soundEffects[sound].m_audioBuffer.Flags = XAUDIO2_END_OF_STREAM;
|
|
m_soundEffects[sound].m_audioBuffer.LoopCount = 0;
|
|
}
|
|
|
|
void Audio::UnloadSoundEffect(const char* pszFilePath)
|
|
{
|
|
int sound = Hash(pszFilePath);
|
|
|
|
UnloadSoundEffect(sound);
|
|
}
|
|
|
|
void Audio::UnloadSoundEffect(unsigned int sound)
|
|
{
|
|
if (m_engineExperiencedCriticalError) {
|
|
return;
|
|
}
|
|
|
|
if (m_soundEffects.end() == m_soundEffects.find(sound))
|
|
return;
|
|
|
|
m_soundEffects[sound].m_soundEffectSourceVoice->DestroyVoice();
|
|
|
|
if (m_soundEffects[sound].m_soundEffectBufferData)
|
|
{
|
|
delete[] m_soundEffects[sound].m_soundEffectBufferData;
|
|
m_soundEffects[sound].m_soundEffectBufferData = NULL;
|
|
}
|
|
|
|
m_soundEffects[sound].m_soundEffectSourceVoice = nullptr;
|
|
m_soundEffects[sound].m_soundEffectStarted = false;//
|
|
ZeroMemory(&m_soundEffects[sound].m_audioBuffer, sizeof(m_soundEffects[sound].m_audioBuffer));
|
|
|
|
m_soundEffects.erase(sound);
|
|
}
|