Hi all. Please brace yourself, this will be a long post.
I made a little arcade game for fun on my spare time. I have actually already added sound: a handful of short samples that are played frequently. The sounds work without glitches and there seem to be no memory leaks, but I am still unsure if my architecture is good.
Main questions:
1) I want to play audio from a separate thread. Does my AudioThread mechanism make sense?
2) Am I using the JUCE Audio classes efficiently? Most examples I’ve seen try to reimplement MixerAudioSource or AudioSourcePlayer, whereas I have merely instantiated them as class members.
The parts of my Controller class which are sound-related look like this:
class Controller
{
public:
enum SoundID
{
SOUND_NONE = 0,
SOUND_CLICK,
SOUND_EXPLODE,
// ... More sounds
};
void QueueSound(SoundID soundID);
private:
class AudioThread : public juce::Thread
{
public:
AudioThread();
void run() override;
void QueueSound(SoundID soundID);
private:
std::atomic<SoundID> m_soundID = { SoundID::SOUND_NONE };
};
AudioThread m_audioThread;
void InitAudio();
void ShutdownAudio();
void PlaySound(SoundID soundID);
juce::AudioDeviceManager m_audioDeviceManager;
juce::AudioSourcePlayer m_audioPlayer;
juce::MixerAudioSource m_audioMixer;
typedef std::unique_ptr<juce::AudioFormatReaderSource> SoundSource;
std::map<SoundID, SoundSource> m_soundSources;
};
The Controller::InitAudio() and Controller::ShutdownAudio() methds are called in the constructor and destructor respectively, and is where the JUCE Audio classes are initialized and configured. This is where I am unsure if this is being done in the most efficient way, since I haven’t fully understood how these different classes relate and interact with each other. Some kind of class or interaction diagram would be cool to have in the docs.
void Controller::InitAudio()
{
// Enable support for WAV files and other commom formats.
juce::AudioFormatManager audioFormatManager;
audioFormatManager.registerBasicFormats();
// Stereo output.
m_audioDeviceManager.initialiseWithDefaultDevices(0, 2);
// Registers audio callback to be used.
m_audioDeviceManager.addAudioCallback(&m_audioPlayer);
juce::AudioIODevice* audioDevice = m_audioDeviceManager.getCurrentAudioDevice();
if (audioDevice)
{
for (int i = SOUND_CLICK; i < SoundID::SOUND_MAX; i++)
{
std::unique_ptr<juce::InputStream> inputStream;
SoundID sId = static_cast<SoundID>(i);
switch (sId)
{
case SOUND_CLICK:
inputStream = std::make_unique<juce::MemoryInputStream>(BinaryData::tap_wav, BinaryData::tap_wavSize, true);
break;
// Other cases....
default:
break;
}
// Create a new AudioSource which gets its data from the binary stream above.
SoundSource source(new juce::AudioFormatReaderSource(audioFormatManager.createReaderFor(std::move(inputStream)), true));
// Store this source in a map to be easily found later, in Controller::PlaySound().
m_soundSources.insert(std::make_pair(sId, std::move(source)));
}
// Audio is to be mixed by m_audioMixer and passed on to
// the AudioSourcePlayer, which then streams it to the AudioIODevice.
m_audioPlayer.setSource(&m_audioMixer);
// Start the AudioThread, which will lay dormant
// until woken up by calls to QueueSound().
m_audioThread.startThread();
}
}
void Controller::ShutdownAudio()
{
// -- TODO: are these really needed?
for (std::map<SoundID, SoundSource>::iterator iter = m_soundSources.begin(); iter != m_soundSources.end(); ++iter)
{
iter->second->releaseResources();
}
m_audioMixer.releaseResources();
m_audioMixer.removeAllInputs();
// --
// Exception in CriticalSection::enter() without this.
m_audioPlayer.setSource(nullptr);
// Attempts to stop the thread running. The threadShouldExit() method will return true,
// and notify() will be called in case the thread is currently waiting.
m_audioThread.stopThread(2000);
}
The Controller::QueueSound method will simply pass on to AudioThread, which will be then woken up. Once awake, AudioThread::run() will call Controller::PlaySound and the sound will be played from that thread.
void Controller::QueueSound(SoundID soundID)
{
m_audioThread.QueueSound(soundID);
}
void Controller::PlaySound(SoundID soundID)
{
juce::AudioIODevice* audioDevice = m_audioDeviceManager.getCurrentAudioDevice();
if (audioDevice && (m_soundSources.count(soundID) != 0))
{
// Set playhead back to the start of the sample,
// and add it to the mixer if not already added before.
// TODO: adding immediately plays it, so not added in InitAudio().
m_soundSources.at(soundID)->setNextReadPosition(0);
m_audioMixer.addInputSource(m_soundSources.at(soundID).get(), false);
}
}
void Controller::AudioThread::run()
{
while (!threadShouldExit())
{
// Just a safety net to avoid constantly triggering the sound,
// should the wait() call not have immediate effect.
// TODO: need this?
if (m_soundID != SOUND_NONE)
{
Controller::GetInstance()->PlaySound(m_soundID);
m_soundID = SOUND_NONE;
}
// Stop the AudioThread until another thread calls notify().
wait(-1);
}
}
void Controller::AudioThread::QueueSound(SoundID soundID)
{
// Wake up the AudioThread, and let it know
// which sound is to be triggered once run() is called.
m_soundID = soundID;
notify();
}
Finally, to trigger a sound from the GUI, I can call:
void MainComponent::mouseDown(const juce::MouseEvent& event)
{
Controller::GetInstance()->QueueSound(Controller::SOUND_CLICK);
}
And you will immediately hear the click sound effect. There is no perceptible delay between the mouse click and the sound, so it appears that waking up the AudioThread with notify(), and then triggering the sound from the AudioThread()::run() method is fast enough.
Thank you for reading this far, hope to hear your comments
Cheers!