Audio: Add dummy device (in case OpenAL fails to load) and unifiate unit tests

This commit is contained in:
Jérôme Leclercq
2022-03-18 19:03:57 +01:00
parent efa2c0a253
commit 82641c6653
30 changed files with 809 additions and 107 deletions

View File

@@ -6,6 +6,7 @@
#include <Nazara/Audio/AudioBuffer.hpp>
#include <Nazara/Audio/AudioSource.hpp>
#include <Nazara/Audio/Config.hpp>
#include <Nazara/Audio/DummyAudioDevice.hpp>
#include <Nazara/Audio/Enums.hpp>
#include <Nazara/Audio/OpenALLibrary.hpp>
#include <Nazara/Audio/Formats/drwavLoader.hpp>
@@ -32,11 +33,12 @@ namespace Nz
* \brief Audio class that represents the module initializer of Audio
*/
Audio::Audio(Config /*config*/) :
ModuleBase("Audio", this)
Audio::Audio(Config config) :
ModuleBase("Audio", this),
m_hasDummyDevice(config.allowDummyDevice)
{
// Load OpenAL
if (!s_openalLibrary.Load())
if (!config.noAudio && !s_openalLibrary.Load())
throw std::runtime_error("failed to load OpenAL");
// Loaders
@@ -49,7 +51,10 @@ namespace Nz
m_soundBufferLoader.RegisterLoader(Loaders::GetSoundBufferLoader_minimp3());
m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_minimp3());
m_defaultDevice = s_openalLibrary.OpenDevice();
if (s_openalLibrary.IsLoaded())
m_defaultDevice = s_openalLibrary.OpenDevice();
else
m_defaultDevice = std::make_shared<DummyAudioDevice>();
}
Audio::~Audio()
@@ -101,17 +106,30 @@ namespace Nz
std::shared_ptr<AudioDevice> Audio::OpenOutputDevice(const std::string& deviceName)
{
if (deviceName == "dummy")
return std::make_shared<DummyAudioDevice>();
return s_openalLibrary.OpenDevice(deviceName.c_str());
}
std::vector<std::string> Audio::QueryInputDevices() const
{
if (!s_openalLibrary.IsLoaded())
return {};
return s_openalLibrary.QueryInputDevices();
}
std::vector<std::string> Audio::QueryOutputDevices() const
{
return s_openalLibrary.QueryOutputDevices();
std::vector<std::string> outputDevices;
if (s_openalLibrary.IsLoaded())
outputDevices = s_openalLibrary.QueryOutputDevices();
if (m_hasDummyDevice)
outputDevices.push_back("dummy");
return outputDevices;
}
Audio* Audio::s_instance = nullptr;

View File

@@ -0,0 +1,50 @@
// Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Audio module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Audio/DummyAudioBuffer.hpp>
#include <Nazara/Audio/Algorithm.hpp>
#include <Nazara/Audio/AudioDevice.hpp>
#include <Nazara/Core/Algorithm.hpp>
#include <Nazara/Audio/Debug.hpp>
namespace Nz
{
AudioFormat DummyAudioBuffer::GetAudioFormat() const
{
return m_format;
}
UInt32 DummyAudioBuffer::GetDuration() const
{
return SafeCast<UInt32>((1000ULL * m_sampleCount / (GetChannelCount(m_format) * m_sampleRate)));
}
UInt64 DummyAudioBuffer::GetSampleCount() const
{
return m_sampleCount;
}
UInt64 DummyAudioBuffer::GetSize() const
{
return m_sampleCount * sizeof(Int16);
}
UInt32 DummyAudioBuffer::GetSampleRate() const
{
return m_sampleRate;
}
bool DummyAudioBuffer::IsCompatibleWith(const AudioDevice& device) const
{
return GetAudioDevice()->GetSubSystemIdentifier() == device.GetSubSystemIdentifier();
}
bool DummyAudioBuffer::Reset(AudioFormat format, UInt64 sampleCount, UInt32 sampleRate, const void* /*samples*/)
{
m_format = format;
m_sampleCount = sampleCount;
m_sampleRate = sampleRate;
return true;
}
}

View File

@@ -0,0 +1,110 @@
// Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Audio module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Audio/DummyAudioDevice.hpp>
#include <Nazara/Audio/DummyAudioBuffer.hpp>
#include <Nazara/Audio/DummyAudioSource.hpp>
#include <cstring>
#include <stdexcept>
#include <Nazara/Audio/Debug.hpp>
namespace Nz
{
DummyAudioDevice::DummyAudioDevice() :
m_listenerRotation(Quaternionf::Identity()),
m_listenerPosition(Vector3f::Zero()),
m_dopplerFactor(1.f),
m_globalVolume(1.f),
m_speedOfSound(343.3f)
{
}
std::shared_ptr<AudioBuffer> DummyAudioDevice::CreateBuffer()
{
return std::make_shared<DummyAudioBuffer>(shared_from_this());
}
std::shared_ptr<AudioSource> DummyAudioDevice::CreateSource()
{
return std::make_shared<DummyAudioSource>(shared_from_this());
}
float DummyAudioDevice::GetDopplerFactor() const
{
return m_dopplerFactor;
}
float DummyAudioDevice::GetGlobalVolume() const
{
return m_globalVolume;
}
Vector3f DummyAudioDevice::GetListenerDirection(Vector3f* up) const
{
if (up)
*up = m_listenerRotation * Vector3f::Up();
return m_listenerRotation * Vector3f::Forward();
}
Vector3f DummyAudioDevice::GetListenerPosition() const
{
return m_listenerPosition;
}
Quaternionf DummyAudioDevice::GetListenerRotation() const
{
return m_listenerRotation;
}
Vector3f DummyAudioDevice::GetListenerVelocity() const
{
return m_listenerVelocity;
}
float DummyAudioDevice::GetSpeedOfSound() const
{
return m_speedOfSound;
}
const void* DummyAudioDevice::GetSubSystemIdentifier() const
{
return this;
}
bool DummyAudioDevice::IsFormatSupported(AudioFormat /*format*/) const
{
return true;
}
void DummyAudioDevice::SetDopplerFactor(float dopplerFactor)
{
m_dopplerFactor = dopplerFactor;
}
void DummyAudioDevice::SetGlobalVolume(float volume)
{
m_globalVolume = volume;
}
void DummyAudioDevice::SetListenerDirection(const Vector3f& direction, const Vector3f& up)
{
m_listenerRotation = Quaternionf::LookAt(direction, up);
}
void DummyAudioDevice::SetListenerPosition(const Vector3f& position)
{
m_listenerPosition = position;
}
void DummyAudioDevice::SetListenerVelocity(const Vector3f& velocity)
{
m_listenerVelocity = velocity;
}
void DummyAudioDevice::SetSpeedOfSound(float speed)
{
m_speedOfSound = speed;
}
}

View File

@@ -0,0 +1,262 @@
// Copyright (C) 2022 Jérôme "Lynix" Leclercq (lynix680@gmail.com)
// This file is part of the "Nazara Engine - Audio module"
// For conditions of distribution and use, see copyright notice in Config.hpp
#include <Nazara/Audio/DummyAudioSource.hpp>
#include <Nazara/Audio/Algorithm.hpp>
#include <Nazara/Audio/DummyAudioBuffer.hpp>
#include <Nazara/Core/Algorithm.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/Log.hpp>
#include <Nazara/Core/StackArray.hpp>
#include <algorithm>
#include <Nazara/Audio/Debug.hpp>
namespace Nz
{
void DummyAudioSource::EnableLooping(bool loop)
{
m_isLooping = loop;
}
void DummyAudioSource::EnableSpatialization(bool spatialization)
{
m_isSpatialized = spatialization;
}
float DummyAudioSource::GetAttenuation() const
{
return m_attenuation;
}
float DummyAudioSource::GetMinDistance() const
{
return m_minDistance;
}
float DummyAudioSource::GetPitch() const
{
return m_pitch;
}
Vector3f DummyAudioSource::GetPosition() const
{
return m_position;
}
UInt32 DummyAudioSource::GetSampleOffset() const
{
UInt64 bufferTime = UpdateTime();
UInt64 sampleOffset = 0;
// All processed buffers count in sample offset
for (const auto& processedBuffer : m_processedBuffers)
sampleOffset += processedBuffer->GetSampleCount() / GetChannelCount(processedBuffer->GetAudioFormat());
if (!m_queuedBuffers.empty())
{
auto& frontBuffer = m_queuedBuffers.front();
UInt64 bufferOffset = bufferTime * frontBuffer->GetSampleRate() / 1000;
UInt64 bufferDuration = frontBuffer->GetSampleCount() / GetChannelCount(frontBuffer->GetAudioFormat());
sampleOffset += std::min(bufferOffset, bufferDuration);
}
return SafeCast<UInt32>(sampleOffset);
}
Vector3f DummyAudioSource::GetVelocity() const
{
return m_velocity;
}
SoundStatus DummyAudioSource::GetStatus() const
{
UpdateTime();
return m_status;
}
float DummyAudioSource::GetVolume() const
{
return m_volume;
}
bool DummyAudioSource::IsLooping() const
{
return m_isLooping;
}
bool DummyAudioSource::IsSpatializationEnabled() const
{
return m_isSpatialized;
}
void DummyAudioSource::QueueBuffer(std::shared_ptr<AudioBuffer> audioBuffer)
{
NazaraAssert(audioBuffer, "invalid buffer");
NazaraAssert(audioBuffer->IsCompatibleWith(*GetAudioDevice()), "incompatible buffer");
m_queuedBuffers.emplace_back(std::static_pointer_cast<DummyAudioBuffer>(audioBuffer));
}
void DummyAudioSource::Pause()
{
m_playClock.Pause();
m_status = SoundStatus::Paused;
}
void DummyAudioSource::Play()
{
if (m_status == SoundStatus::Paused)
m_playClock.Unpause();
else
{
// playing or stopped, restart
RequeueBuffers();
m_playClock.Restart();
}
m_status = SoundStatus::Playing;
}
void DummyAudioSource::SetAttenuation(float attenuation)
{
m_attenuation = attenuation;
}
void DummyAudioSource::SetBuffer(std::shared_ptr<AudioBuffer> audioBuffer)
{
NazaraAssert(audioBuffer->IsCompatibleWith(*GetAudioDevice()), "incompatible buffer");
m_queuedBuffers.clear();
m_queuedBuffers.emplace_back(std::static_pointer_cast<DummyAudioBuffer>(audioBuffer));
m_processedBuffers.clear();
}
void DummyAudioSource::SetMinDistance(float minDistance)
{
m_minDistance = minDistance;
}
void DummyAudioSource::SetPitch(float pitch)
{
m_pitch = pitch;
}
void DummyAudioSource::SetPosition(const Vector3f& position)
{
m_position = position;
}
void DummyAudioSource::SetSampleOffset(UInt32 offset)
{
RequeueBuffers();
if (m_queuedBuffers.empty())
return;
std::size_t processedBufferIndex = 0;
for (; processedBufferIndex < m_queuedBuffers.size(); ++processedBufferIndex)
{
UInt32 bufferFrameCount = m_queuedBuffers[processedBufferIndex]->GetSampleCount() / GetChannelCount(m_queuedBuffers[processedBufferIndex]->GetAudioFormat());
if (offset < bufferFrameCount)
break;
offset -= bufferFrameCount;
m_processedBuffers.emplace_back(std::move(m_queuedBuffers[processedBufferIndex]));
}
m_queuedBuffers.erase(m_queuedBuffers.begin(), m_queuedBuffers.begin() + processedBufferIndex);
assert(!m_queuedBuffers.empty());
UInt64 timeOffset = 1'000'000ULL * offset / m_queuedBuffers.front()->GetSampleRate();
m_playClock.Restart(timeOffset, m_playClock.IsPaused());
}
void DummyAudioSource::SetVelocity(const Vector3f& velocity)
{
m_velocity = velocity;
}
void DummyAudioSource::SetVolume(float volume)
{
m_volume = volume;
}
void DummyAudioSource::Stop()
{
m_playClock.Restart(0, true);
}
std::shared_ptr<AudioBuffer> DummyAudioSource::TryUnqueueProcessedBuffer()
{
UpdateTime();
if (m_processedBuffers.empty())
return {};
auto processedBuffer = std::move(m_processedBuffers.front());
m_processedBuffers.erase(m_processedBuffers.begin());
return processedBuffer;
}
void DummyAudioSource::UnqueueAllBuffers()
{
m_processedBuffers.clear();
m_queuedBuffers.clear();
Stop();
}
void DummyAudioSource::RequeueBuffers()
{
// Put back all processed buffers in the queued buffer queue (for simplicity)
if (!m_processedBuffers.empty())
{
m_queuedBuffers.resize(m_processedBuffers.size() + m_queuedBuffers.size());
std::move(m_queuedBuffers.begin(), m_queuedBuffers.begin() + m_processedBuffers.size(), m_queuedBuffers.begin() + m_processedBuffers.size());
std::move(m_processedBuffers.begin(), m_processedBuffers.end(), m_queuedBuffers.begin());
m_processedBuffers.clear();
}
}
UInt64 DummyAudioSource::UpdateTime() const
{
UInt64 currentTime = m_playClock.GetMilliseconds();
while (!m_queuedBuffers.empty() && currentTime >= m_queuedBuffers.front()->GetDuration())
{
auto processedBuffer = std::move(m_queuedBuffers.front());
m_queuedBuffers.erase(m_queuedBuffers.begin());
currentTime -= processedBuffer->GetDuration();
m_processedBuffers.emplace_back(std::move(processedBuffer));
}
if (m_queuedBuffers.empty())
{
// If looping, replay processed buffers
if (m_isLooping)
{
while (!m_processedBuffers.empty())
{
auto queuedBuffer = std::move(m_processedBuffers.front());
m_processedBuffers.erase(m_processedBuffers.begin());
m_queuedBuffers.emplace_back(std::move(queuedBuffer));
if (m_queuedBuffers.back()->GetDuration() > currentTime)
break;
currentTime -= m_queuedBuffers.back()->GetDuration();
}
}
else
m_status = SoundStatus::Stopped;
}
m_playClock.Restart(currentTime * 1000, m_playClock.IsPaused()); //< Adjust time
return currentTime;
}
}

View File

@@ -133,7 +133,6 @@ namespace Nz
std::lock_guard<std::mutex> lock(m_bufferLock);
UInt32 sampleOffset = m_source->GetSampleOffset();
return static_cast<UInt32>((1000ULL * (sampleOffset + (m_processedSamples / GetChannelCount(m_stream->GetFormat())))) / m_sampleRate);
}
@@ -173,6 +172,8 @@ namespace Nz
{
NazaraAssert(m_stream, "Music not created");
std::lock_guard<std::mutex> lock(m_bufferLock);
SoundStatus status = m_source->GetStatus();
// To compensate any delays (or the timelaps between Play() and the thread startup)
@@ -250,6 +251,8 @@ namespace Nz
*/
void Music::Pause()
{
std::lock_guard<std::mutex> lock(m_bufferLock);
m_source->Pause();
}
@@ -285,24 +288,7 @@ namespace Nz
}
}
else
{
std::mutex mutex;
std::condition_variable cv;
// Starting streaming thread
m_streaming = true;
std::exception_ptr exceptionPtr;
std::unique_lock<std::mutex> lock(mutex);
m_thread = std::thread(&Music::MusicThread, this, std::ref(cv), std::ref(mutex), std::ref(exceptionPtr));
// Wait until thread signal it has properly started (or an error occurred)
cv.wait(lock);
if (exceptionPtr)
std::rethrow_exception(exceptionPtr);
}
StartThread(false);
}
/*!
@@ -319,9 +305,10 @@ namespace Nz
NazaraAssert(m_stream, "Music not created");
bool isPlaying = m_streaming;
bool isPaused = GetStatus() == SoundStatus::Paused;
if (isPlaying)
Stop();
StopThread();
UInt64 sampleOffset = UInt64(offset) * m_sampleRate * GetChannelCount(m_stream->GetFormat()) / 1000ULL;
@@ -329,7 +316,7 @@ namespace Nz
m_streamOffset = sampleOffset;
if (isPlaying)
Play();
StartThread(isPaused);
}
/*!
@@ -380,7 +367,7 @@ namespace Nz
return sampleRead != sampleCount; // End of stream (Does not happen when looping)
}
void Music::MusicThread(std::condition_variable& cv, std::mutex& m, std::exception_ptr& err)
void Music::MusicThread(std::condition_variable& cv, std::mutex& m, std::exception_ptr& err, bool startPaused)
{
// Allocation of streaming buffers
CallOnExit unqueueBuffers([&]
@@ -406,8 +393,14 @@ namespace Nz
cv.notify_all();
return;
}
m_source->Play();
if (startPaused)
{
// little hack to start paused (required by SetPlayingOffset)
m_source->Pause();
m_source->SetSampleOffset(0);
}
CallOnExit stopSource([&]
{
@@ -424,6 +417,11 @@ namespace Nz
// Reading loop (Filling new buffers as playing)
while (m_streaming)
{
// Wait until buffers are processed
std::this_thread::sleep_for(std::chrono::milliseconds(50));
std::lock_guard<std::mutex> lock(m_bufferLock);
SoundStatus status = m_source->GetStatus();
if (status == SoundStatus::Stopped)
{
@@ -432,24 +430,37 @@ namespace Nz
break;
}
// We treat read buffers
while (std::shared_ptr<AudioBuffer> buffer = m_source->TryUnqueueProcessedBuffer())
{
std::lock_guard<std::mutex> lock(m_bufferLock);
m_processedSamples += buffer->GetSampleCount();
// We treat read buffers
while (std::shared_ptr<AudioBuffer> buffer = m_source->TryUnqueueProcessedBuffer())
{
m_processedSamples += buffer->GetSampleCount();
if (FillAndQueueBuffer(std::move(buffer)))
break;
}
if (FillAndQueueBuffer(std::move(buffer)))
break;
}
// We go back to sleep
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
}
void Music::StartThread(bool startPaused)
{
std::mutex mutex;
std::condition_variable cv;
// Starting streaming thread
m_streaming = true;
std::exception_ptr exceptionPtr;
std::unique_lock<std::mutex> lock(mutex);
m_thread = std::thread(&Music::MusicThread, this, std::ref(cv), std::ref(mutex), std::ref(exceptionPtr), startPaused);
// Wait until thread signal it has properly started (or an error occurred)
cv.wait(lock);
if (exceptionPtr)
std::rethrow_exception(exceptionPtr);
}
void Music::StopThread()
{
if (m_streaming)

View File

@@ -17,7 +17,7 @@ namespace Nz
m_library.alDeleteBuffers(1, &m_bufferId);
}
UInt32 OpenALBuffer::GetSampleCount() const
UInt64 OpenALBuffer::GetSampleCount() const
{
GetDevice().MakeContextCurrent();
@@ -25,21 +25,21 @@ namespace Nz
m_library.alGetBufferi(m_bufferId, AL_BITS, &bits);
m_library.alGetBufferi(m_bufferId, AL_SIZE, &size);
UInt32 sampleCount = 0;
UInt64 sampleCount = 0;
if (bits != 0)
sampleCount += (8 * SafeCast<UInt32>(size)) / SafeCast<UInt32>(bits);
sampleCount += (8 * SafeCast<UInt64>(size)) / SafeCast<UInt64>(bits);
return sampleCount;
}
UInt32 OpenALBuffer::GetSize() const
UInt64 OpenALBuffer::GetSize() const
{
GetDevice().MakeContextCurrent();
ALint size;
m_library.alGetBufferi(m_bufferId, AL_SIZE, &size);
return SafeCast<UInt32>(size);
return SafeCast<UInt64>(size);
}
UInt32 OpenALBuffer::GetSampleRate() const
@@ -49,7 +49,7 @@ namespace Nz
ALint sampleRate;
m_library.alGetBufferi(m_bufferId, AL_FREQUENCY, &sampleRate);
return SafeCast<UInt32>(sampleRate);
return SafeCast<UInt64>(sampleRate);
}
bool OpenALBuffer::IsCompatibleWith(const AudioDevice& device) const

View File

@@ -100,7 +100,7 @@ namespace Nz
/*!
* \brief Gets the global volume
* \return Float between [0, inf) with 100.f being the default
* \return Float between [0, inf) with 1.f being the default
*/
float OpenALDevice::GetGlobalVolume() const
{
@@ -109,7 +109,7 @@ namespace Nz
ALfloat gain = 0.f;
m_library.alGetListenerf(AL_GAIN, &gain);
return gain * 100.f;
return gain;
}
/*!

View File

@@ -4,6 +4,7 @@
#include <Nazara/Audio/OpenALLibrary.hpp>
#include <Nazara/Core/Algorithm.hpp>
#include <Nazara/Core/CallOnExit.hpp>
#include <Nazara/Core/Error.hpp>
#include <Nazara/Core/ErrorFlags.hpp>
#include <Nazara/Core/Log.hpp>
@@ -20,6 +21,8 @@ namespace Nz
{
Unload();
CallOnExit unloadOnFailure([this] { Unload(); });
#if defined(NAZARA_PLATFORM_WINDOWS)
std::array libs{
"soft_oal.dll",
@@ -69,15 +72,21 @@ namespace Nz
continue;
}
unloadOnFailure.Reset();
return true;
}
m_hasCaptureSupport = alcIsExtensionPresent(nullptr, "ALC_EXT_CAPTURE");
NazaraError("failed to load OpenAL library");
return false;
}
std::vector<std::string> OpenALLibrary::QueryInputDevices()
{
if (!m_hasCaptureSupport)
return {};
return ParseDevices(alcGetString(nullptr, ALC_CAPTURE_DEVICE_SPECIFIER));
}

View File

@@ -131,10 +131,10 @@ namespace Nz
{
GetDevice().MakeContextCurrent();
ALint relative;
m_library.alGetSourcei(m_sourceId, AL_LOOPING, &relative);
ALint looping;
m_library.alGetSourcei(m_sourceId, AL_LOOPING, &looping);
return relative == AL_FALSE;
return looping == AL_TRUE;
}
bool OpenALSource::IsSpatializationEnabled() const

View File

@@ -125,7 +125,7 @@ namespace Nz
* Restarts the clock, putting it's time counter back to zero (as if the clock got constructed).
* It also compute the elapsed microseconds since the last Restart() call without any time loss (a problem that the combination of GetElapsedMicroseconds and Restart have).
*/
UInt64 Clock::Restart()
UInt64 Clock::Restart(UInt64 startingValue, bool paused)
{
Nz::UInt64 now = GetElapsedMicroseconds();
@@ -133,9 +133,9 @@ namespace Nz
if (!m_paused)
elapsedTime += (now - m_refTime);
m_elapsedTime = 0;
m_elapsedTime = startingValue;
m_refTime = now;
m_paused = false;
m_paused = paused;
return elapsedTime;
}