NazaraEngine/src/Nazara/Audio/Music.cpp

536 lines
13 KiB
C++

// Copyright (C) 2024 Jérôme "SirLynix" 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/Music.hpp>
#include <Nazara/Audio/Algorithm.hpp>
#include <Nazara/Audio/Audio.hpp>
#include <Nazara/Audio/AudioBuffer.hpp>
#include <Nazara/Audio/AudioDevice.hpp>
#include <Nazara/Audio/AudioSource.hpp>
#include <Nazara/Audio/SoundStream.hpp>
#include <Nazara/Core/ThreadExt.hpp>
#include <NazaraUtils/CallOnExit.hpp>
#include <array>
#include <chrono>
#include <optional>
#include <Nazara/Audio/Debug.hpp>
namespace Nz
{
/*!
* \ingroup audio
* \class Nz::Music
* \brief Audio class that represents a music
*
* \remark Module Audio needs to be initialized to use this class
*/
Music::Music() :
Music(*Audio::Instance()->GetDefaultDevice())
{
}
Music::Music(AudioDevice& device) :
SoundEmitter(device),
m_streaming(false),
m_bufferCount(2),
m_looping(false)
{
}
/*!
* \brief Destructs the object and calls Destroy
*
* \see Destroy
*/
Music::~Music()
{
Destroy();
}
/*!
* \brief Creates a music with a sound stream
* \return true if creation was successful
*
* \param soundStream Sound stream which is the source for the music
*/
bool Music::Create(std::shared_ptr<SoundStream> soundStream)
{
NazaraAssert(soundStream, "Invalid stream");
Destroy();
AudioFormat format = soundStream->GetFormat();
m_sampleRate = soundStream->GetSampleRate();
m_audioFormat = soundStream->GetFormat();
m_chunkSamples.resize(GetChannelCount(format) * m_sampleRate); // One second of samples
m_stream = std::move(soundStream);
SeekToSampleOffset(0);
return true;
}
/*!
* \brief Destroys the current music and frees resources
*
* \remark If the Music is playing, it is stopped first.
*/
void Music::Destroy()
{
StopThread();
}
/*!
* \brief Enables the looping of the music
*
* \param loop Should music loop
*
* \remark Music must be valid when calling this function
*/
void Music::EnableLooping(bool loop)
{
std::lock_guard<std::recursive_mutex> lock(m_sourceLock);
m_looping = loop;
}
/*!
* \brief Gets the duration of the music
* \return Duration of the music in milliseconds
*
* \remark Music must be valid when calling this function
*/
Time Music::GetDuration() const
{
NazaraAssert(m_stream, "Music not created");
return m_stream->GetDuration();
}
/*!
* \brief Gets the format of the music
* \return Enumeration of type AudioFormat (mono, stereo, ...)
*
* \remark Music must be valid when calling this function
*/
AudioFormat Music::GetFormat() const
{
NazaraAssert(m_stream, "Music not created");
return m_stream->GetFormat();
}
/*!
* \brief Gets the current playing offset of the music
* \return Time offset
*/
Time Music::GetPlayingOffset() const
{
NazaraAssert(m_stream, "Music not created");
// Prevent music thread from enqueuing new buffers while we're getting the count
std::lock_guard<std::recursive_mutex> lock(m_sourceLock);
if (!m_streaming)
return Time::Zero();
Time playingOffset = m_source->GetPlayingOffset();
Time processedTime = Time::Microseconds(1'000'000ll * m_processedSamples / (GetChannelCount(m_stream->GetFormat()) * m_sampleRate));
playingOffset += processedTime;
Time sampleCount = m_stream->GetDuration();
if (playingOffset > sampleCount)
{
if (m_looping)
playingOffset %= sampleCount;
else
playingOffset = Time::Zero(); //< stopped
}
return playingOffset;
}
/*!
* \brief Gets the number of samples in the music
* \return Count of samples (number of seconds * sample rate * channel count)
*
* \remark Music must be valid when calling this function
*/
UInt64 Music::GetSampleCount() const
{
NazaraAssert(m_stream, "Music not created");
return m_stream->GetSampleCount();
}
/*!
* \brief Gets the current offset in the music
* \return Offset in samples
*/
UInt64 Music::GetSampleOffset() const
{
NazaraAssert(m_stream, "Music not created");
if (!m_streaming)
return 0;
// Prevent music thread from enqueuing new buffers while we're getting the count
std::lock_guard<std::recursive_mutex> lock(m_sourceLock);
UInt64 sampleOffset = m_processedSamples + m_source->GetSampleOffset();
UInt64 sampleCount = m_stream->GetSampleCount();
if (sampleOffset > sampleCount)
{
if (m_looping)
sampleOffset %= sampleCount;
else
sampleOffset = 0; //< stopped
}
return sampleOffset;
}
/*!
* \brief Gets the rates of sample in the music
* \return Rate of sample in Hertz (Hz)
*
* \remark Music must be valid when calling this function
*/
UInt32 Music::GetSampleRate() const
{
NazaraAssert(m_stream, "Music not created");
return m_sampleRate;
}
/*!
* \brief Gets the status of the music
* \return Enumeration of type SoundStatus (Playing, Stopped, ...)
*
* \remark Music must be valid when calling this function
*/
SoundStatus Music::GetStatus() const
{
NazaraAssert(m_stream, "Music not created");
std::lock_guard<std::recursive_mutex> lock(m_sourceLock);
return m_source->GetStatus();
}
/*!
* \brief Checks whether the music is looping
* \return true if it is the case
*
* \remark Music must be valid when calling this function
*/
bool Music::IsLooping() const
{
std::lock_guard<std::recursive_mutex> lock(m_sourceLock);
return m_looping;
}
/*!
* \brief Opens the music from a file
* \return true if the file was successfully opened
*
* \param filePath Path to the file
* \param params Parameters for the music
*/
bool Music::OpenFromFile(const std::filesystem::path& filePath, const SoundStreamParams& params)
{
if (std::shared_ptr<SoundStream> soundStream = SoundStream::OpenFromFile(filePath, params))
return Create(std::move(soundStream));
else
return false;
}
/*!
* \brief Opens the music from memory
* \return true if loading is successful
*
* \param data Raw memory
* \param size Size of the memory
* \param params Parameters for the music
*
* \remark The memory pointer must stay valid (accessible) as long as the music is playing
*/
bool Music::OpenFromMemory(const void* data, std::size_t size, const SoundStreamParams& params)
{
if (std::shared_ptr<SoundStream> soundStream = SoundStream::OpenFromMemory(data, size, params))
return Create(std::move(soundStream));
else
return false;
}
/*!
* \brief Loads the music from stream
* \return true if loading is successful
*
* \param stream Stream to the music
* \param params Parameters for the music
*
* \remark The stream must stay valid as long as the music is playing
*/
bool Music::OpenFromStream(Stream& stream, const SoundStreamParams& params)
{
if (std::shared_ptr<SoundStream> soundStream = SoundStream::OpenFromStream(stream, params))
return Create(std::move(soundStream));
else
return false;
}
/*!
* \brief Pauses the music
*
* \remark Music must be valid when calling this function
*/
void Music::Pause()
{
std::lock_guard<std::recursive_mutex> lock(m_sourceLock);
m_source->Pause();
}
/*!
* \brief Plays the music
*
* Plays/Resume the music.
* If the music is currently playing, resets the playing offset to the beginning offset.
* If the music is currently paused, resumes the playing.
* If the music is currently stopped, starts the playing at the previously set playing offset.
*
* \remark Music must be valid when calling this function
*/
void Music::Play()
{
NazaraAssert(m_stream, "Music not created");
// Maybe we are already playing
if (m_streaming)
{
std::lock_guard<std::recursive_mutex> lock(m_sourceLock);
switch (GetStatus())
{
case SoundStatus::Playing:
SeekToSampleOffset(0);
break;
case SoundStatus::Paused:
m_source->Play();
break;
default:
break; // We shouldn't be stopped
}
}
else
{
// Ensure we're restarting
StopThread();
// Special case of SetPlayingOffset(end) before Play(), restart from beginning
if (m_streamOffset >= m_stream->GetSampleCount())
m_streamOffset = 0;
StartThread(false);
}
}
/*!
* \brief Changes the playing offset of the music
*
* If the music is not playing, this sets the playing offset for the next Play call
*
* \param offset The offset in samples
*
* \remark Music must be valid when calling this function
*/
void Music::SeekToSampleOffset(UInt64 offset)
{
NazaraAssert(m_stream, "Music not created");
bool isPlaying = m_streaming;
bool isPaused = GetStatus() == SoundStatus::Paused;
if (isPlaying)
StopThread();
UInt64 sampleOffset = offset * GetChannelCount(m_stream->GetFormat());
m_processedSamples = sampleOffset;
m_streamOffset = sampleOffset;
if (isPlaying)
StartThread(isPaused);
}
/*!
* \brief Stops the music
*
* \remark Music must be valid when calling this function
*/
void Music::Stop()
{
StopThread();
SeekToSampleOffset(0);
}
bool Music::FillAndQueueBuffer(std::shared_ptr<AudioBuffer> buffer)
{
std::size_t sampleCount = m_chunkSamples.size();
std::size_t sampleRead = 0;
{
std::lock_guard<std::mutex> lock(m_stream->GetMutex());
m_stream->Seek(m_streamOffset);
// Fill the buffer by reading from the stream
for (;;)
{
sampleRead += m_stream->Read(&m_chunkSamples[sampleRead], sampleCount - sampleRead);
if (sampleRead < sampleCount && m_looping)
{
// In case we read less than expected, assume we reached the end of the stream and seek back to the beginning
m_stream->Seek(0);
continue;
}
// Either we read the size we wanted, either we're not looping
break;
}
m_streamOffset = m_stream->Tell();
}
// Update the buffer on the AudioDevice and queue it if we got any data
if (sampleRead > 0)
{
buffer->Reset(m_audioFormat, sampleRead, m_sampleRate, &m_chunkSamples[0]);
m_source->QueueBuffer(buffer);
}
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, bool startPaused)
{
SetCurrentThreadName("MusicThread");
std::optional<std::lock_guard<std::recursive_mutex>> exitLock;
// Allocation of streaming buffers
CallOnExit unqueueBuffers([&]
{
m_source->UnqueueAllBuffers();
});
try
{
for (std::size_t i = 0; i < m_bufferCount; ++i)
{
std::shared_ptr<AudioBuffer> buffer = m_source->GetAudioDevice()->CreateBuffer();
if (FillAndQueueBuffer(std::move(buffer)))
break; // We have reached the end of the stream, there is no use to add new buffers
}
}
catch (const std::exception&)
{
err = std::current_exception();
std::unique_lock<std::mutex> lock(m);
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([&]
{
// Stop playing of the sound (in the case where it has not been already done)
m_source->Stop();
});
// Signal we're good
{
std::unique_lock<std::mutex> lock(m);
m_musicStarted = true;
cv.notify_all();
} // m & cv no longer exists from here
// From now, the source can be accessed from another thread, lock it before others destructors
CallOnExit lockSource([&]
{
exitLock.emplace(m_sourceLock);
});
// 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::recursive_mutex> lock(m_sourceLock);
SoundStatus status = m_source->GetStatus();
if (status == SoundStatus::Stopped)
{
// The reading has stopped, we have reached the end of the stream
m_streaming = false;
break;
}
// We treat read buffers
while (std::shared_ptr<AudioBuffer> buffer = m_source->TryUnqueueProcessedBuffer())
{
m_processedSamples += buffer->GetSampleCount();
if (FillAndQueueBuffer(std::move(buffer)))
break;
}
}
}
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_musicStarted = false;
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, [&] { return exceptionPtr || m_musicStarted; });
if (exceptionPtr)
std::rethrow_exception(exceptionPtr);
}
void Music::StopThread()
{
if (m_streaming)
m_streaming = false;
if (m_thread.joinable())
m_thread.join();
}
}