diff --git a/src/Nazara/Audio/Audio.cpp b/src/Nazara/Audio/Audio.cpp index 6817463bc..e97d51aa0 100644 --- a/src/Nazara/Audio/Audio.cpp +++ b/src/Nazara/Audio/Audio.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,8 @@ namespace Nz m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_minimp3()); m_soundBufferLoader.RegisterLoader(Loaders::GetSoundBufferLoader_sndfile()); m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_sndfile()); + m_soundBufferLoader.RegisterLoader(Loaders::GetSoundBufferLoader_drwav()); + m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_drwav()); } Audio::~Audio() diff --git a/src/Nazara/Audio/Formats/drwavLoader.cpp b/src/Nazara/Audio/Formats/drwavLoader.cpp new file mode 100644 index 000000000..f8aa59cba --- /dev/null +++ b/src/Nazara/Audio/Formats/drwavLoader.cpp @@ -0,0 +1,353 @@ +// Copyright (C) 2020 Jérôme Leclercq +// 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 + +#define DR_WAV_IMPLEMENTATION +#define DR_WAV_NO_STDIO +#include + +#include + +namespace Nz +{ + namespace + { + std::optional GuessFormat(UInt32 channelCount) + { + switch (channelCount) + { + case 1: + return AudioFormat::U16_Mono; + + case 2: + return AudioFormat::U16_Stereo; + + case 4: + return AudioFormat::U16_Quad; + + case 6: + return AudioFormat::U16_5_1; + + case 7: + return AudioFormat::U16_6_1; + + case 8: + return AudioFormat::U16_7_1; + + default: + return std::nullopt; + } + } + + std::string ErrToString(int errCode) + { + switch (errCode) + { + case DRWAV_SUCCESS: return "no error"; + case DRWAV_INVALID_ARGS: return "wrong parameters"; + case DRWAV_ERROR: return "generic error"; + case DRWAV_INVALID_OPERATION: return "invalid operation"; + case DRWAV_OUT_OF_MEMORY: return "out of memory"; + default: return "unknown error"; + } + } + + std::size_t ReadCallback(void* pUserData, void* pBufferOut, size_t bytesToRead) + { + Stream* stream = static_cast(pUserData); + return static_cast(stream->Read(pBufferOut, bytesToRead)); + } + + drwav_bool32 SeekCallback(void* pUserData, int offset, drwav_seek_origin origin) + { + Stream* stream = static_cast(pUserData); + switch (origin) + { + case drwav_seek_origin_start: + return stream->SetCursorPos(offset); + + case drwav_seek_origin_current: + return (stream->Read(nullptr, static_cast(offset)) != 0); + + default: + NazaraInternalError("Seek mode not handled"); + return false; + } + } + + bool IsSupported(const std::string_view& extension) + { + return extension == "wav"; + } + + Ternary CheckWav(Stream& stream) + { + drwav wav; + if (!drwav_init(&wav, &ReadCallback, &SeekCallback, &stream, nullptr)) + return Ternary::False; + + drwav_uninit(&wav); + return Ternary::True; + } + + std::shared_ptr LoadSoundBuffer(Stream& stream, const SoundBufferParams& parameters) + { + drwav wav; + if (!drwav_init(&wav, &ReadCallback, &SeekCallback, &stream, nullptr)) + { + NazaraError("failed to decode wav stream"); + return {}; + } + + CallOnExit uninitOnExit([&] { drwav_uninit(&wav); }); + + std::optional formatOpt = GuessFormat(wav.channels); + if (!formatOpt) + { + NazaraError("unexpected channel count: " + std::to_string(wav.channels)); + return {}; + } + + AudioFormat format = *formatOpt; + + UInt64 sampleCount = wav.totalPCMFrameCount * wav.channels; + std::unique_ptr samples = std::make_unique(sampleCount); //< std::vector would default-init to zero + + drwav_read_pcm_frames_s16(&wav, wav.totalPCMFrameCount, samples.get()); + + if (parameters.forceMono && format != AudioFormat::U16_Mono) + { + MixToMono(samples.get(), samples.get(), static_cast(wav.channels), wav.totalPCMFrameCount); + + format = AudioFormat::U16_Mono; + sampleCount = wav.totalPCMFrameCount; + } + + return std::make_shared(format, sampleCount, wav.sampleRate, samples.get()); + } + + class drwavStream : public SoundStream + { + public: + drwavStream() : + m_readSampleCount(0) + { + std::memset(&m_decoder, 0, sizeof(m_decoder)); + } + + ~drwavStream() + { + drwav_uninit(&m_decoder); + } + + UInt32 GetDuration() const override + { + return m_duration; + } + + AudioFormat GetFormat() const override + { + if (m_mixToMono) + return AudioFormat::U16_Mono; + else + return m_format; + } + + std::mutex& GetMutex() override + { + return m_mutex; + } + + UInt64 GetSampleCount() const override + { + return m_sampleCount; + } + + UInt32 GetSampleRate() const override + { + return m_sampleRate; + } + + bool Open(const std::filesystem::path& filePath, bool forceMono) + { + std::unique_ptr file = std::make_unique(); + if (!file->Open(filePath, OpenMode::ReadOnly)) + { + NazaraError("failed to open stream from file: " + Error::GetLastError()); + return false; + } + + m_ownedStream = std::move(file); + return Open(*m_ownedStream, forceMono); + } + + bool Open(const void* data, std::size_t size, bool forceMono) + { + m_ownedStream = std::make_unique(data, size); + return Open(*m_ownedStream, forceMono); + } + + bool Open(Stream& stream, bool forceMono) + { + if (!drwav_init(&m_decoder, &ReadCallback, &SeekCallback, &stream, nullptr)) + { + NazaraError("failed to decode wav stream"); + return {}; + } + + CallOnExit resetOnError([this] + { + drwav_uninit(&m_decoder); + std::memset(&m_decoder, 0, sizeof(m_decoder)); + }); + + std::optional formatOpt = GuessFormat(m_decoder.channels); + if (!formatOpt) + { + NazaraError("unexpected channel count: " + std::to_string(m_decoder.channels)); + return false; + } + + m_format = *formatOpt; + + m_duration = static_cast(1000ULL * m_decoder.totalPCMFrameCount / (m_decoder.sampleRate * m_decoder.channels)); + m_sampleCount = m_decoder.totalPCMFrameCount * m_decoder.channels; + m_sampleRate = m_decoder.sampleRate; + + // Mixing to mono will be done on the fly + if (forceMono && m_format != AudioFormat::U16_Mono) + { + m_mixToMono = true; + m_sampleCount = m_decoder.totalPCMFrameCount; + } + else + m_mixToMono = false; + + resetOnError.Reset(); + + return true; + } + + UInt64 Read(void* buffer, UInt64 sampleCount) override + { + // Convert to mono in the fly if necessary + if (m_mixToMono) + { + // Keep a buffer to the side to prevent allocation + m_mixBuffer.resize(sampleCount * m_decoder.channels); + std::size_t readSample = drwav_read_pcm_frames_s16(&m_decoder, sampleCount, static_cast(m_mixBuffer.data())); + m_readSampleCount += readSample; + + MixToMono(m_mixBuffer.data(), static_cast(buffer), m_decoder.channels, sampleCount); + + return readSample; + } + else + { + UInt64 readSample = drwav_read_pcm_frames_s16(&m_decoder, sampleCount / m_decoder.channels, static_cast(buffer)); + m_readSampleCount += readSample; + + return readSample * m_decoder.channels; + } + } + + void Seek(UInt64 offset) override + { + drwav_seek_to_pcm_frame(&m_decoder, offset); + m_readSampleCount = offset; + } + + UInt64 Tell() override + { + return m_readSampleCount; + } + + private: + std::unique_ptr m_ownedStream; + std::vector m_mixBuffer; + AudioFormat m_format; + drwav m_decoder; + bool m_mixToMono; + std::mutex m_mutex; + UInt32 m_duration; + UInt32 m_sampleRate; + UInt64 m_readSampleCount; + UInt64 m_sampleCount; + }; + + std::shared_ptr LoadSoundStreamFile(const std::filesystem::path& filePath, const SoundStreamParams& parameters) + { + std::shared_ptr soundStream = std::make_shared(); + if (!soundStream->Open(filePath, parameters.forceMono)) + { + NazaraError("failed to open sound stream"); + return {}; + } + + return soundStream; + } + + std::shared_ptr LoadSoundStreamMemory(const void* data, std::size_t size, const SoundStreamParams& parameters) + { + std::shared_ptr soundStream = std::make_shared(); + if (!soundStream->Open(data, size, parameters.forceMono)) + { + NazaraError("failed to open music stream"); + return {}; + } + + return soundStream; + } + + std::shared_ptr LoadSoundStreamStream(Stream& stream, const SoundStreamParams& parameters) + { + std::shared_ptr soundStream = std::make_shared(); + if (!soundStream->Open(stream, parameters.forceMono)) + { + NazaraError("failed to open music stream"); + return {}; + } + + return soundStream; + } + } + + namespace Loaders + { + SoundBufferLoader::Entry GetSoundBufferLoader_drwav() + { + SoundBufferLoader::Entry loaderEntry; + loaderEntry.extensionSupport = IsSupported; + loaderEntry.streamChecker = [](Stream& stream, const SoundBufferParams&) { return CheckWav(stream); }; + loaderEntry.streamLoader = LoadSoundBuffer; + + return loaderEntry; + } + + SoundStreamLoader::Entry GetSoundStreamLoader_drwav() + { + SoundStreamLoader::Entry loaderEntry; + loaderEntry.extensionSupport = IsSupported; + loaderEntry.streamChecker = [](Stream& stream, const SoundStreamParams&) { return CheckWav(stream); }; + loaderEntry.fileLoader = LoadSoundStreamFile; + loaderEntry.memoryLoader = LoadSoundStreamMemory; + loaderEntry.streamLoader = LoadSoundStreamStream; + + return loaderEntry; + } + } +} diff --git a/src/Nazara/Audio/Formats/drwavLoader.hpp b/src/Nazara/Audio/Formats/drwavLoader.hpp new file mode 100644 index 000000000..49d4adb17 --- /dev/null +++ b/src/Nazara/Audio/Formats/drwavLoader.hpp @@ -0,0 +1,20 @@ +// Copyright (C) 2020 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_LOADERS_DRWAV_HPP +#define NAZARA_LOADERS_DRWAV_HPP + +#include +#include +#include + +namespace Nz::Loaders +{ + SoundBufferLoader::Entry GetSoundBufferLoader_drwav(); + SoundStreamLoader::Entry GetSoundStreamLoader_drwav(); +} + +#endif diff --git a/xmake.lua b/xmake.lua index 1fa09f333..3b6719101 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,7 +1,7 @@ local modules = { Audio = { Deps = {"NazaraCore"}, - Packages = {"libsndfile", "minimp3"} + Packages = {"dr_wav", "libsndfile", "minimp3"} }, Core = {}, Graphics = { @@ -86,7 +86,7 @@ local modules = { add_repositories("local-repo xmake-repo") -add_requires("chipmunk2d", "freetype", "libsndfile", "libsdl", "minimp3", "stb") +add_requires("chipmunk2d", "dr_wav", "freetype", "libsndfile", "libsdl", "minimp3", "stb") add_requires("newtondynamics", { debug = is_plat("windows") and is_mode("debug") }) -- Newton doesn't like compiling in Debug on Linux set_project("NazaraEngine")