From 79b6b87379fbc975dfe580fc19667c0bdb6feca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Leclercq?= Date: Sun, 30 May 2021 02:32:06 +0200 Subject: [PATCH] Audio: Add .ogg loader (using libvorbisfile) --- src/Nazara/Audio/Audio.cpp | 3 + src/Nazara/Audio/Formats/libvorbisLoader.cpp | 428 +++++++++++++++++++ src/Nazara/Audio/Formats/libvorbisLoader.hpp | 20 + xmake.lua | 4 +- 4 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 src/Nazara/Audio/Formats/libvorbisLoader.cpp create mode 100644 src/Nazara/Audio/Formats/libvorbisLoader.hpp diff --git a/src/Nazara/Audio/Audio.cpp b/src/Nazara/Audio/Audio.cpp index e97d51aa0..3686476d2 100644 --- a/src/Nazara/Audio/Audio.cpp +++ b/src/Nazara/Audio/Audio.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -42,6 +43,8 @@ namespace Nz m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_sndfile()); m_soundBufferLoader.RegisterLoader(Loaders::GetSoundBufferLoader_drwav()); m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_drwav()); + m_soundBufferLoader.RegisterLoader(Loaders::GetSoundBufferLoader_libvorbis()); + m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_libvorbis()); } Audio::~Audio() diff --git a/src/Nazara/Audio/Formats/libvorbisLoader.cpp b/src/Nazara/Audio/Formats/libvorbisLoader.cpp new file mode 100644 index 000000000..7d9ca80e5 --- /dev/null +++ b/src/Nazara/Audio/Formats/libvorbisLoader.cpp @@ -0,0 +1,428 @@ +// 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 +#include +#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::size_t ReadCallback(void* ptr, size_t size, size_t nmemb, void* datasource) + { + Stream* stream = static_cast(datasource); + return static_cast(stream->Read(ptr, size * nmemb)); + } + + int SeekCallback(void* datasource, ogg_int64_t offset, int whence) + { + Stream* stream = static_cast(datasource); + switch (whence) + { + case SEEK_CUR: + stream->Read(nullptr, static_cast(offset)); + break; + + case SEEK_END: + stream->SetCursorPos(stream->GetSize() + offset); // offset is negative here + break; + + case SEEK_SET: + stream->SetCursorPos(offset); + break; + + default: + NazaraInternalError("Seek mode not handled"); + return false; + } + + return 0; + } + + long TellCallback(void* datasource) + { + Stream* stream = static_cast(datasource); + return static_cast(stream->GetCursorPos()); + } + + static ov_callbacks s_callbacks = { + &ReadCallback, + &SeekCallback, + nullptr, + &TellCallback + }; + + + std::string ErrToString(int errCode) + { + switch (errCode) + { + case 0: return "no error"; + case OV_EBADHEADER: return "invalid Vorbis bitstream header"; + case OV_EBADLINK: return "an invalid stream section was supplied to libvorbisfile, or the requested link is corrupt"; + case OV_EFAULT: return "internal logic fault"; + case OV_EINVAL: return "an invalid stream section was supplied to libvorbisfile, or the requested link is corrupt"; + case OV_ENOTVORBIS: return "bitstream does not contain any Vorbis data"; + case OV_EREAD: return "a read from media returned an error"; + case OV_EVERSION: return "Vorbis version mismatch"; + case OV_HOLE: return "there was an interruption in the data"; + default: return "unknown error"; + } + } + + UInt64 ReadOgg(OggVorbis_File* file, void* buffer, UInt64 sampleCount) + { + constexpr int bigendian = (GetPlatformEndianness() == Endianness::LittleEndian) ? 0 : 1; + + char* ptr = reinterpret_cast(buffer); + UInt64 remainingBytes = sampleCount * sizeof(Int16); + do + { + long readBytes = ov_read(file, ptr, int(remainingBytes), bigendian, 2, 1, nullptr); + if (readBytes == 0) + break; //< End of file + + if (readBytes < 0) + { + NazaraError("an error occurred while reading file: " + ErrToString(readBytes)); + return 0; + } + + assert(readBytes > 0 && readBytes <= remainingBytes); + + ptr += readBytes; + remainingBytes -= readBytes; + } + while (remainingBytes > 0); + + return sampleCount - remainingBytes / sizeof(Int16); + } + + bool IsSupported(const std::string_view& extension) + { + return extension == "ogg"; + } + + Ternary CheckOgg(Stream& stream) + { + OggVorbis_File file; + if (ov_test_callbacks(&stream, &file, nullptr, 0, s_callbacks) != 0) + return Ternary::False; + + ov_clear(&file); + return Ternary::True; + } + + std::shared_ptr LoadSoundBuffer(Stream& stream, const SoundBufferParams& parameters) + { + OggVorbis_File file; + int err = ov_open_callbacks(&stream, &file, nullptr, 0, s_callbacks); + if (err != 0) + { + NazaraError(ErrToString(err)); + return {}; + } + + CallOnExit clearOnExit([&] { ov_clear(&file); }); + + vorbis_info* info = ov_info(&file, -1); + assert(info); + + std::optional formatOpt = GuessFormat(info->channels); + if (!formatOpt) + { + NazaraError("unexpected channel count: " + std::to_string(info->channels)); + return {}; + } + + AudioFormat format = *formatOpt; + + UInt64 frameCount = UInt64(ov_pcm_total(&file, -1)); + UInt64 sampleCount = UInt64(frameCount * info->channels); + std::unique_ptr samples = std::make_unique(sampleCount); //< std::vector would default-init to zero + + UInt64 readSample = ReadOgg(&file, samples.get(), sampleCount); + if (readSample == 0) + return {}; + + if (readSample != sampleCount) + { + NazaraError("failed to read the whole file"); + return {}; + } + + if (parameters.forceMono && format != AudioFormat::U16_Mono) + { + MixToMono(samples.get(), samples.get(), static_cast(info->channels), frameCount); + + format = AudioFormat::U16_Mono; + sampleCount = frameCount; + } + + return std::make_shared(format, sampleCount, info->rate, samples.get()); + } + + class libvorbisStream : public SoundStream + { + public: + libvorbisStream() + { + m_decoder.datasource = nullptr; + } + + ~libvorbisStream() + { + if (m_decoder.datasource) + ov_clear(&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) + { + int err = ov_open_callbacks(&stream, &m_decoder, nullptr, 0, s_callbacks); + if (err != 0) + { + NazaraError(ErrToString(err)); + return {}; + } + + CallOnExit clearOnError([&] + { + ov_clear(&m_decoder); + m_decoder.datasource = nullptr; + }); + + vorbis_info* info = ov_info(&m_decoder, -1); + assert(info); + + std::optional formatOpt = GuessFormat(info->channels); + if (!formatOpt) + { + NazaraError("unexpected channel count: " + std::to_string(info->channels)); + return {}; + } + + m_format = *formatOpt; + + UInt64 frameCount = UInt64(ov_pcm_total(&m_decoder, -1)); + + m_channelCount = info->channels; + m_duration = UInt32(1000ULL * frameCount / info->rate); + m_sampleCount = UInt64(frameCount * info->channels); + m_sampleRate = info->rate; + + // Mixing to mono will be done on the fly + if (forceMono && m_format != AudioFormat::U16_Mono) + { + m_mixToMono = true; + m_sampleCount = frameCount; + } + else + m_mixToMono = false; + + clearOnError.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_channelCount); + + std::size_t readSample = ReadOgg(&m_decoder, m_mixBuffer.data(), sampleCount * m_channelCount); + MixToMono(m_mixBuffer.data(), static_cast(buffer), m_channelCount, sampleCount); + + return readSample / m_channelCount; + } + else + { + UInt64 readSample = ReadOgg(&m_decoder, buffer, sampleCount); + return readSample; + } + } + + void Seek(UInt64 offset) override + { + if (m_mixToMono) + offset *= m_channelCount; + + ov_pcm_seek(&m_decoder, Int64(offset)); + } + + UInt64 Tell() override + { + UInt64 offset = UInt64(ov_pcm_tell(&m_decoder)); + if (m_mixToMono) + offset /= m_channelCount; + + return offset; + } + + private: + std::mutex m_mutex; + std::unique_ptr m_ownedStream; + std::vector m_mixBuffer; + AudioFormat m_format; + OggVorbis_File m_decoder; + UInt32 m_channelCount; + UInt32 m_duration; + UInt32 m_sampleRate; + UInt64 m_sampleCount; + bool m_mixToMono; + }; + + 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_libvorbis() + { + SoundBufferLoader::Entry loaderEntry; + loaderEntry.extensionSupport = IsSupported; + loaderEntry.streamChecker = [](Stream& stream, const SoundBufferParams&) { return CheckOgg(stream); }; + loaderEntry.streamLoader = LoadSoundBuffer; + + return loaderEntry; + } + + SoundStreamLoader::Entry GetSoundStreamLoader_libvorbis() + { + SoundStreamLoader::Entry loaderEntry; + loaderEntry.extensionSupport = IsSupported; + loaderEntry.streamChecker = [](Stream& stream, const SoundStreamParams&) { return CheckOgg(stream); }; + loaderEntry.fileLoader = LoadSoundStreamFile; + loaderEntry.memoryLoader = LoadSoundStreamMemory; + loaderEntry.streamLoader = LoadSoundStreamStream; + + return loaderEntry; + } + } +} + diff --git a/src/Nazara/Audio/Formats/libvorbisLoader.hpp b/src/Nazara/Audio/Formats/libvorbisLoader.hpp new file mode 100644 index 000000000..b82d53bad --- /dev/null +++ b/src/Nazara/Audio/Formats/libvorbisLoader.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_LIBVORBIS_HPP +#define NAZARA_LOADERS_LIBVORBIS_HPP + +#include +#include +#include + +namespace Nz::Loaders +{ + SoundBufferLoader::Entry GetSoundBufferLoader_libvorbis(); + SoundStreamLoader::Entry GetSoundStreamLoader_libvorbis(); +} + +#endif diff --git a/xmake.lua b/xmake.lua index 1993e6209..f9b12adfb 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,7 +1,7 @@ local modules = { Audio = { Deps = {"NazaraCore"}, - Packages = {"dr_wav", "libsndfile", "minimp3"} + Packages = {"dr_wav", "libogg", "libsndfile", "minimp3"} }, Core = { Custom = function () @@ -92,7 +92,7 @@ local modules = { add_repositories("local-repo xmake-repo") -add_requires("chipmunk2d", "dr_wav", "freetype", "libsndfile", "libsdl", "minimp3", "stb") +add_requires("chipmunk2d", "dr_wav", "freetype", "libogg", "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")