536 lines
13 KiB
C++
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();
|
|
}
|
|
}
|