// 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 #include #include #include #include #include #include #include #include #include #include #include #include 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) { 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 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 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 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 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 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::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::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::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 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 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 buffer) { std::size_t sampleCount = m_chunkSamples.size(); std::size_t sampleRead = 0; { std::lock_guard 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> exitLock; // Allocation of streaming buffers CallOnExit unqueueBuffers([&] { m_source->UnqueueAllBuffers(); }); try { for (std::size_t i = 0; i < m_bufferCount; ++i) { std::shared_ptr 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 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 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 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 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 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(); } }