From d121393267173a279ff8b355d939fe9f46b148e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Leclercq?= Date: Sat, 29 May 2021 19:34:36 +0200 Subject: [PATCH] Audio: Add mp3 support + new demo --- bin/resources/file_example_MP3_700KB.txt | 1 + examples/DopplerEffect/main.cpp | 7 - examples/PlayMusic/main.cpp | 48 +++ examples/PlayMusic/xmake.lua | 5 + src/Nazara/Audio/Audio.cpp | 3 + src/Nazara/Audio/Formats/minimp3Loader.cpp | 368 +++++++++++++++++++++ src/Nazara/Audio/Formats/minimp3Loader.hpp | 20 ++ xmake.lua | 4 +- 8 files changed, 447 insertions(+), 9 deletions(-) create mode 100644 bin/resources/file_example_MP3_700KB.txt create mode 100644 examples/PlayMusic/main.cpp create mode 100644 examples/PlayMusic/xmake.lua create mode 100644 src/Nazara/Audio/Formats/minimp3Loader.cpp create mode 100644 src/Nazara/Audio/Formats/minimp3Loader.hpp diff --git a/bin/resources/file_example_MP3_700KB.txt b/bin/resources/file_example_MP3_700KB.txt new file mode 100644 index 000000000..fca4d8ad8 --- /dev/null +++ b/bin/resources/file_example_MP3_700KB.txt @@ -0,0 +1 @@ +https://file-examples.com/index.php/sample-audio-files/sample-mp3-download/ \ No newline at end of file diff --git a/examples/DopplerEffect/main.cpp b/examples/DopplerEffect/main.cpp index 7ecd473b8..8dfe7a4de 100644 --- a/examples/DopplerEffect/main.cpp +++ b/examples/DopplerEffect/main.cpp @@ -24,14 +24,7 @@ int main() if (!std::filesystem::is_directory(resourceDir) && std::filesystem::is_directory(".." / resourceDir)) resourceDir = ".." / resourceDir; - // NzKeyboard nécessite l'initialisation du module Utilitaire Nz::Modules audio; - /*if (!audio) - { - std::cout << "Failed to initialize audio module" << std::endl; - std::getchar(); - return 1; - }*/ Nz::Sound sound; if (!sound.LoadFromFile(resourceDir / "siren.wav")) diff --git a/examples/PlayMusic/main.cpp b/examples/PlayMusic/main.cpp new file mode 100644 index 000000000..c28b586ab --- /dev/null +++ b/examples/PlayMusic/main.cpp @@ -0,0 +1,48 @@ +/* +** DopplerEffect - Introduction à la lecture de son spatialisé (+ démonstration de l'effet doppler) +** Prérequis: Aucun +** Utilisation du noyau et du module audio +** Présente: +** - Chargement, lecture et positionnement d'un son +** - Gestion basique d'une horloge +** - Gestion basique de position 3D +*/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +int main() +{ + std::filesystem::path resourceDir = "resources"; + if (!std::filesystem::is_directory(resourceDir) && std::filesystem::is_directory(".." / resourceDir)) + resourceDir = ".." / resourceDir; + + Nz::Modules audio; + + Nz::SoundStreamParams streamParams; + streamParams.forceMono = false; + + Nz::Music music; + if (!music.OpenFromFile(resourceDir / "file_example_MP3_700KB.mp3", streamParams)) + { + std::cout << "Failed to load sound" << std::endl; + std::getchar(); + return EXIT_FAILURE; + } + + music.Play(); + + std::cout << "Playing sound..." << std::endl; + + while (music.IsPlaying()) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + return EXIT_SUCCESS; +} diff --git a/examples/PlayMusic/xmake.lua b/examples/PlayMusic/xmake.lua new file mode 100644 index 000000000..32a81eb4c --- /dev/null +++ b/examples/PlayMusic/xmake.lua @@ -0,0 +1,5 @@ +target("PlayMusic") + set_group("Examples") + set_kind("binary") + add_deps("NazaraAudio") + add_files("main.cpp") diff --git a/src/Nazara/Audio/Audio.cpp b/src/Nazara/Audio/Audio.cpp index 77bb50cc0..6817463bc 100644 --- a/src/Nazara/Audio/Audio.cpp +++ b/src/Nazara/Audio/Audio.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,8 @@ namespace Nz SetListenerDirection(Vector3f::Forward()); // Loaders + m_soundBufferLoader.RegisterLoader(Loaders::GetSoundBufferLoader_minimp3()); + m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_minimp3()); m_soundBufferLoader.RegisterLoader(Loaders::GetSoundBufferLoader_sndfile()); m_soundStreamLoader.RegisterLoader(Loaders::GetSoundStreamLoader_sndfile()); } diff --git a/src/Nazara/Audio/Formats/minimp3Loader.cpp b/src/Nazara/Audio/Formats/minimp3Loader.cpp new file mode 100644 index 000000000..5c6ac22d1 --- /dev/null +++ b/src/Nazara/Audio/Formats/minimp3Loader.cpp @@ -0,0 +1,368 @@ +// 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 + +#define MINIMP3_IMPLEMENTATION +#define MINIMP3_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 5: + return AudioFormat::U16_5_1; + + case 6: + return AudioFormat::U16_6_1; + + case 7: + return AudioFormat::U16_7_1; + + default: + return std::nullopt; + } + } + + std::string ErrToString(int errCode) + { + switch (errCode) + { + case 0: return "no error"; + case MP3D_E_PARAM: return "wrong parameters"; + case MP3D_E_MEMORY: return "not enough memory"; + case MP3D_E_IOERROR: return "I/O error"; + case MP3D_E_USER: return "aborted"; + case MP3D_E_DECODE: return "decoding error"; + default: return "unknown error"; + } + } + + size_t ReadCallback(void* buf, size_t size, void* user_data) + { + Stream* stream = static_cast(user_data); + return static_cast(stream->Read(buf, size)); + } + + int SeekCallback(uint64_t position, void* user_data) + { + Stream* stream = static_cast(user_data); + return (stream->SetCursorPos(position)) ? 0 : MP3D_E_IOERROR; + } + + bool IsSupported(const std::string_view& extension) + { + return extension == "mp3"; + } + + Ternary CheckMp3(Stream& stream) + { + mp3dec_io_t io; + io.read = &ReadCallback; + io.read_data = &stream; + io.seek = &SeekCallback; + io.seek_data = &stream; + + std::vector buffer(MINIMP3_BUF_SIZE); + return (mp3dec_detect_cb(&io, buffer.data(), buffer.size()) == 0) ? Ternary::True : Ternary::False; + } + + std::shared_ptr LoadSoundBuffer(Stream& stream, const SoundBufferParams& parameters) + { + static_assert(std::is_same_v); + + mp3dec_io_t io; + io.read = &ReadCallback; + io.read_data = &stream; + io.seek = &SeekCallback; + io.seek_data = &stream; + + struct UserData + { + std::vector samples; + }; + + UserData userdata; + + mp3dec_t dec; + mp3dec_file_info_t info; + std::vector buffer(MINIMP3_BUF_SIZE); + int err = mp3dec_load_cb(&dec, &io, buffer.data(), buffer.size(), &info, nullptr, &userdata); + if (err != 0) + { + NazaraError(ErrToString(err)); + return {}; + } + + CallOnExit freeBuffer([&] { std::free(info.buffer); }); + + std::optional formatOpt = GuessFormat(info.channels); + if (!formatOpt) + { + NazaraError("unexpected channel count: " + std::to_string(info.channels)); + return {}; + } + + AudioFormat format = *formatOpt; + + UInt32 sampleCount = static_cast(info.samples); + + if (parameters.forceMono && format != AudioFormat::U16_Mono) + { + UInt32 frameCount = UInt32(info.samples / info.channels); + MixToMono(info.buffer, info.buffer, static_cast(info.channels), frameCount); + + format = AudioFormat::U16_Mono; + sampleCount = frameCount; + } + + return std::make_shared(format, sampleCount, info.hz, info.buffer); + } + + class minimp3Stream : public SoundStream + { + public: + minimp3Stream() : + m_readSampleCount(0) + { + std::memset(&m_decoder, 0, sizeof(m_decoder)); + } + + ~minimp3Stream() + { + mp3dec_ex_close(&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) + { + m_io.read = &ReadCallback; + m_io.read_data = &stream; + m_io.seek = &SeekCallback; + m_io.seek_data = &stream; + + int err = mp3dec_ex_open_cb(&m_decoder, &m_io, MP3D_SEEK_TO_SAMPLE); + if (err != 0) + { + NazaraError(ErrToString(err)); + return {}; + } + + CallOnExit resetOnError([this] + { + mp3dec_ex_close(&m_decoder); + std::memset(&m_decoder, 0, sizeof(m_decoder)); + }); + + std::optional formatOpt = GuessFormat(m_decoder.info.channels); + if (!formatOpt) + { + NazaraError("unexpected channel count: " + std::to_string(m_decoder.info.channels)); + return false; + } + + m_format = *formatOpt; + + m_duration = static_cast(1000ULL * m_decoder.samples / (m_decoder.info.hz * m_decoder.info.channels)); + m_sampleCount = m_decoder.samples; + m_sampleRate = m_decoder.info.hz; + + // Mixing to mono will be done on the fly + if (forceMono && m_format != AudioFormat::U16_Mono) + { + m_mixToMono = true; + m_sampleCount = static_cast(m_decoder.samples / m_decoder.info.channels); + } + 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) + { + UInt32 channelCount = GetChannelCount(m_format); + + // Keep a buffer to the side to prevent allocation + m_mixBuffer.resize(channelCount * sampleCount); + std::size_t readSample = mp3dec_ex_read(&m_decoder, m_mixBuffer.data(), channelCount * sampleCount); + m_readSampleCount += readSample; + + MixToMono(m_mixBuffer.data(), static_cast(buffer), channelCount, sampleCount); + + return readSample / channelCount; + } + else + { + UInt64 readSample = mp3dec_ex_read(&m_decoder, static_cast(buffer), sampleCount); + m_readSampleCount += readSample; + + return readSample; + } + } + + void Seek(UInt64 offset) override + { + mp3dec_ex_seek(&m_decoder, offset); + } + + UInt64 Tell() override + { + return m_readSampleCount; + } + + private: + std::unique_ptr m_ownedStream; + std::vector m_mixBuffer; + AudioFormat m_format; + mp3dec_ex_t m_decoder; + mp3dec_io_t m_io; + 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_minimp3() + { + SoundBufferLoader::Entry loaderEntry; + loaderEntry.extensionSupport = IsSupported; + loaderEntry.streamChecker = [](Stream& stream, const SoundBufferParams&) { return CheckMp3(stream); }; + loaderEntry.streamLoader = LoadSoundBuffer; + + return loaderEntry; + } + + SoundStreamLoader::Entry GetSoundStreamLoader_minimp3() + { + SoundStreamLoader::Entry loaderEntry; + loaderEntry.extensionSupport = IsSupported; + loaderEntry.streamChecker = [](Stream& stream, const SoundStreamParams&) { return CheckMp3(stream); }; + loaderEntry.fileLoader = LoadSoundStreamFile; + loaderEntry.memoryLoader = LoadSoundStreamMemory; + loaderEntry.streamLoader = LoadSoundStreamStream; + + return loaderEntry; + } + } +} diff --git a/src/Nazara/Audio/Formats/minimp3Loader.hpp b/src/Nazara/Audio/Formats/minimp3Loader.hpp new file mode 100644 index 000000000..4d40ef00b --- /dev/null +++ b/src/Nazara/Audio/Formats/minimp3Loader.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_MINIMP3_HPP +#define NAZARA_LOADERS_MINIMP3_HPP + +#include +#include +#include + +namespace Nz::Loaders +{ + SoundBufferLoader::Entry GetSoundBufferLoader_minimp3(); + SoundStreamLoader::Entry GetSoundStreamLoader_minimp3(); +} + +#endif diff --git a/xmake.lua b/xmake.lua index f0bf9ebd0..1fa09f333 100644 --- a/xmake.lua +++ b/xmake.lua @@ -1,7 +1,7 @@ local modules = { Audio = { Deps = {"NazaraCore"}, - Packages = {"libsndfile"} + Packages = {"libsndfile", "minimp3"} }, Core = {}, Graphics = { @@ -86,7 +86,7 @@ local modules = { add_repositories("local-repo xmake-repo") -add_requires("chipmunk2d", "freetype", "libsndfile", "libsdl", "stb") +add_requires("chipmunk2d", "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")