// Copyright (C) 2022 Jérôme "Lynix" 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 #define MINIMP3_IMPLEMENTATION #define MINIMP3_NO_STDIO #include #include namespace Nz { namespace { std::string MP3ErrorToString(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 MP3ReadCallback(void* buf, size_t size, void* user_data) { Stream* stream = static_cast(user_data); return static_cast(stream->Read(buf, size)); } int MP3SeekCallback(uint64_t position, void* user_data) { Stream* stream = static_cast(user_data); return (stream->SetCursorPos(position)) ? 0 : MP3D_E_IOERROR; } bool IsMP3Supported(const std::string_view& extension) { return extension == ".mp3"; } Result, ResourceLoadingError> LoadMP3SoundBuffer(Stream& stream, const SoundBufferParams& parameters) { static_assert(std::is_same_v); mp3dec_io_t io; io.read = &MP3ReadCallback; io.read_data = &stream; io.seek = &MP3SeekCallback; io.seek_data = &stream; struct UserData { std::vector samples; }; UserData userdata; mp3dec_t dec; mp3dec_file_info_t info; Nz::UInt64 cursorPos = stream.GetCursorPos(); std::unique_ptr buffer = std::make_unique(MINIMP3_BUF_SIZE); if (mp3dec_detect_cb(&io, buffer.get(), MINIMP3_BUF_SIZE) != 0) return Err(ResourceLoadingError::Unrecognized); stream.SetCursorPos(cursorPos); int err = mp3dec_load_cb(&dec, &io, buffer.get(), MINIMP3_BUF_SIZE, &info, nullptr, &userdata); if (err != 0) { NazaraError(MP3ErrorToString(err)); return Err(ResourceLoadingError::DecodingError); } CallOnExit freeBuffer([&] { std::free(info.buffer); }); std::optional formatOpt = GuessAudioFormat(info.channels); if (!formatOpt) { NazaraError("unexpected channel count: " + std::to_string(info.channels)); return Err(ResourceLoadingError::Unsupported); } AudioFormat format = *formatOpt; UInt64 sampleCount = UInt64(info.samples); if (parameters.forceMono && format != AudioFormat::I16_Mono) { UInt64 frameCount = UInt64(info.samples / info.channels); MixToMono(info.buffer, info.buffer, UInt32(info.channels), frameCount); format = AudioFormat::I16_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::I16_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; } Result Open(const std::filesystem::path& filePath, const SoundStreamParams& parameters) { std::unique_ptr file = std::make_unique(); if (!file->Open(filePath, OpenMode::ReadOnly)) { NazaraError("failed to open stream from file: " + Error::GetLastError()); return Err(ResourceLoadingError::FailedToOpenFile); } m_ownedStream = std::move(file); return Open(*m_ownedStream, parameters); } Result Open(const void* data, std::size_t size, const SoundStreamParams& parameters) { m_ownedStream = std::make_unique(data, size); return Open(*m_ownedStream, parameters); } Result Open(Stream& stream, const SoundStreamParams& parameters) { m_io.read = &MP3ReadCallback; m_io.read_data = &stream; m_io.seek = &MP3SeekCallback; m_io.seek_data = &stream; Nz::UInt64 cursorPos = stream.GetCursorPos(); std::unique_ptr buffer = std::make_unique(MINIMP3_BUF_SIZE); if (mp3dec_detect_cb(&m_io, buffer.get(), MINIMP3_BUF_SIZE) != 0) return Err(ResourceLoadingError::Unrecognized); stream.SetCursorPos(cursorPos); int err = mp3dec_ex_open_cb(&m_decoder, &m_io, MP3D_SEEK_TO_SAMPLE); if (err != 0) { NazaraError(MP3ErrorToString(err)); return Err(ResourceLoadingError::DecodingError); } CallOnExit resetOnError([this] { mp3dec_ex_close(&m_decoder); std::memset(&m_decoder, 0, sizeof(m_decoder)); }); std::optional formatOpt = GuessAudioFormat(m_decoder.info.channels); if (!formatOpt) { NazaraError("unexpected channel count: " + std::to_string(m_decoder.info.channels)); return Err(ResourceLoadingError::Unsupported); } 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 (parameters.forceMono && m_format != AudioFormat::I16_Mono) { m_mixToMono = true; m_sampleCount = static_cast(m_decoder.samples / m_decoder.info.channels); } else m_mixToMono = false; resetOnError.Reset(); return Ok(); } 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); m_readSampleCount = offset; } UInt64 Tell() override { return m_readSampleCount; } private: std::mutex m_mutex; std::unique_ptr m_ownedStream; std::vector m_mixBuffer; AudioFormat m_format; mp3dec_ex_t m_decoder; mp3dec_io_t m_io; UInt32 m_duration; UInt32 m_sampleRate; UInt64 m_readSampleCount; UInt64 m_sampleCount; bool m_mixToMono; }; Result, ResourceLoadingError> LoadMP3SoundStreamFile(const std::filesystem::path& filePath, const SoundStreamParams& parameters) { std::shared_ptr soundStream = std::make_shared(); Result status = soundStream->Open(filePath, parameters); return status.Map([&] { return std::move(soundStream); }); } Result, ResourceLoadingError> LoadMP3SoundStreamMemory(const void* data, std::size_t size, const SoundStreamParams& parameters) { std::shared_ptr soundStream = std::make_shared(); Result status = soundStream->Open(data, size, parameters); return status.Map([&] { return std::move(soundStream); }); } Result, ResourceLoadingError> LoadMP3SoundStreamStream(Stream& stream, const SoundStreamParams& parameters) { std::shared_ptr soundStream = std::make_shared(); Result status = soundStream->Open(stream, parameters); return status.Map([&] { return std::move(soundStream); }); } } namespace Loaders { SoundBufferLoader::Entry GetSoundBufferLoader_minimp3() { SoundBufferLoader::Entry loaderEntry; loaderEntry.extensionSupport = IsMP3Supported; loaderEntry.streamLoader = LoadMP3SoundBuffer; loaderEntry.parameterFilter = [](const SoundBufferParams& parameters) { bool skip; if (parameters.custom.GetBooleanParameter("SkipBuiltinMP3Loader", &skip) && skip) return false; return true; }; return loaderEntry; } SoundStreamLoader::Entry GetSoundStreamLoader_minimp3() { SoundStreamLoader::Entry loaderEntry; loaderEntry.extensionSupport = IsMP3Supported; loaderEntry.fileLoader = LoadMP3SoundStreamFile; loaderEntry.memoryLoader = LoadMP3SoundStreamMemory; loaderEntry.streamLoader = LoadMP3SoundStreamStream; loaderEntry.parameterFilter = [](const SoundStreamParams& parameters) { bool skip; if (parameters.custom.GetBooleanParameter("SkipBuiltinMP3Loader", &skip) && skip) return false; return true; }; return loaderEntry; } } }