diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d72f520c4..826d63b08 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,7 +128,7 @@ jobs: run: | CLANG_VERSION=$(clang++ --version | sed -n 's/.*version \([0-9]\+\)\..*/\1/p') echo "CLANG_VERSION=$CLANG_VERSION" >> $GITHUB_ENV - sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libxi-dev libudev-dev libflac-dev libvorbis-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox ccache gcovr ${{ matrix.platform.name == 'Linux Clang' && 'llvm-$CLANG_VERSION' || '' }} + sudo apt-get update && sudo apt-get install xorg-dev libxrandr-dev libxcursor-dev libxi-dev libudev-dev libflac-dev libvorbis-dev libopus-dev libopusfile-dev libgl1-mesa-dev libegl1-mesa-dev libdrm-dev libgbm-dev xvfb fluxbox ccache gcovr ${{ matrix.platform.name == 'Linux Clang' && 'llvm-$CLANG_VERSION' || '' }} - name: Remove ALSA Library if: runner.os == 'Linux' && matrix.platform.name != 'Android' diff --git a/examples/sound/Sound.cpp b/examples/sound/Sound.cpp index 88ef1709a..d050c5531 100644 --- a/examples/sound/Sound.cpp +++ b/examples/sound/Sound.cpp @@ -85,6 +85,9 @@ int main() // Play a sound playSound(); + // Play music from an opus file + playMusic("error.opus"); + // Play music from an ogg file playMusic("doodle_pop.ogg"); diff --git a/examples/sound/resources/error.opus b/examples/sound/resources/error.opus new file mode 100644 index 000000000..55823645b Binary files /dev/null and b/examples/sound/resources/error.opus differ diff --git a/include/SFML/Audio/InputSoundFile.hpp b/include/SFML/Audio/InputSoundFile.hpp index 523b8e597..9f69b1205 100644 --- a/include/SFML/Audio/InputSoundFile.hpp +++ b/include/SFML/Audio/InputSoundFile.hpp @@ -108,7 +108,8 @@ public: //////////////////////////////////////////////////////////// /// \brief Open a sound file from the disk for reading /// - /// The supported audio formats are: WAV (PCM only), OGG/Vorbis, FLAC, MP3. + /// The supported audio formats are: WAV (PCM only), OGG/Vorbis, Opus, + /// FLAC, and MP3. /// The supported sample sizes for FLAC and WAV are 8, 16, 24 and 32 bit. /// /// Because of minimp3_ex limitation, for MP3 files with big (>16kb) APEv2 tag, @@ -126,7 +127,8 @@ public: //////////////////////////////////////////////////////////// /// \brief Open a sound file in memory for reading /// - /// The supported audio formats are: WAV (PCM only), OGG/Vorbis, FLAC. + /// The supported audio formats are: WAV (PCM only), OGG/Vorbis, Opus, + /// FLAC, and MP3. /// The supported sample sizes for FLAC and WAV are 8, 16, 24 and 32 bit. /// /// \param data Pointer to the file data in memory @@ -140,7 +142,8 @@ public: //////////////////////////////////////////////////////////// /// \brief Open a sound file from a custom stream for reading /// - /// The supported audio formats are: WAV (PCM only), OGG/Vorbis, FLAC. + /// The supported audio formats are: WAV (PCM only), OGG/Vorbis, Opus, + /// FLAC, and MP3. /// The supported sample sizes for FLAC and WAV are 8, 16, 24 and 32 bit. /// /// \param stream Source stream to read from diff --git a/src/SFML/Audio/CMakeLists.txt b/src/SFML/Audio/CMakeLists.txt index 7567205c8..db5904327 100644 --- a/src/SFML/Audio/CMakeLists.txt +++ b/src/SFML/Audio/CMakeLists.txt @@ -48,6 +48,8 @@ set(CODECS_SRC ${SRCROOT}/SoundFileReaderMp3.cpp ${SRCROOT}/SoundFileReaderOgg.hpp ${SRCROOT}/SoundFileReaderOgg.cpp + ${SRCROOT}/SoundFileReaderOpus.hpp + ${SRCROOT}/SoundFileReaderOpus.cpp ${SRCROOT}/SoundFileReaderWav.hpp ${SRCROOT}/SoundFileReaderWav.cpp ${INCROOT}/SoundFileWriter.hpp @@ -55,6 +57,8 @@ set(CODECS_SRC ${SRCROOT}/SoundFileWriterFlac.cpp ${SRCROOT}/SoundFileWriterOgg.hpp ${SRCROOT}/SoundFileWriterOgg.cpp + ${SRCROOT}/SoundFileWriterOpus.hpp + ${SRCROOT}/SoundFileWriterOpus.cpp ${SRCROOT}/SoundFileWriterWav.hpp ${SRCROOT}/SoundFileWriterWav.cpp ) @@ -94,10 +98,23 @@ else() set(INSTALL_DOCS OFF) set(INSTALL_PKG_CONFIG_MODULE OFF) set(INSTALL_PKGCONFIG_MODULES OFF) + set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF) set(WITH_FORTIFY_SOURCE OFF) set(WITH_STACK_PROTECTOR OFF) set(WITH_AVX OFF) # LLVM/Clang on Windows has issues with AVX2 + SET(OP_DISABLE_HTTP ON) + SET(OP_DISABLE_EXAMPLES ON) + SET(OP_DISABLE_DOCS ON) + FetchContent_Declare(opus + GIT_REPOSITORY https://github.com/xiph/opus.git + GIT_TAG v1.5.2 + GIT_SHALLOW ON + # patch out parts we don't want of the Opus CMake configuration + # - feature summary + # - installing headers + # - add CMAKE_DEBUG_POSTFIX + PATCH_COMMAND ${CMAKE_COMMAND} -DOPUS_DIR=${FETCHCONTENT_BASE_DIR}/opus-src -P ${PROJECT_SOURCE_DIR}/tools/opus/PatchOpus.cmake) FetchContent_Declare(ogg GIT_REPOSITORY https://github.com/xiph/ogg.git GIT_TAG v1.3.5 @@ -106,6 +123,13 @@ else() # - installing headers & pkgconfig files # - add CMAKE_DEBUG_POSTFIX PATCH_COMMAND ${CMAKE_COMMAND} -DOGG_DIR=${FETCHCONTENT_BASE_DIR}/ogg-src -P ${PROJECT_SOURCE_DIR}/tools/ogg/PatchOgg.cmake) + FetchContent_Declare(opusfile + GIT_REPOSITORY https://github.com/xiph/opusfile.git + GIT_TAG 9d718345ce03b2fad5d7d28e0bcd1cc69ab2b166 + # patch out parts we don't want of the Opusfile CMake configuration + # - installing headers + # - add CMAKE_DEBUG_POSTFIX + PATCH_COMMAND ${CMAKE_COMMAND} -DOPUSFILE_DIR=${FETCHCONTENT_BASE_DIR}/opusfile-src -P ${PROJECT_SOURCE_DIR}/tools/opus/PatchOpusfile.cmake) FetchContent_Declare(flac GIT_REPOSITORY https://github.com/xiph/flac.git GIT_TAG 1.4.3 @@ -124,20 +148,22 @@ else() # - installing headers & pkgconfig files # - add CMAKE_DEBUG_POSTFIX PATCH_COMMAND ${CMAKE_COMMAND} -DVORBIS_DIR=${FETCHCONTENT_BASE_DIR}/vorbis-src -P ${PROJECT_SOURCE_DIR}/tools/vorbis/PatchVorbis.cmake) - FetchContent_MakeAvailable(ogg flac vorbis) + FetchContent_MakeAvailable(opus ogg opusfile flac vorbis) - set_target_properties(ogg FLAC vorbis vorbisenc vorbisfile PROPERTIES FOLDER "Dependencies") + set_target_properties(opus ogg opusfile FLAC vorbis vorbisenc vorbisfile PROPERTIES FOLDER "Dependencies") # if building SFML as a shared library and linking our dependencies in # as static libraries we need to build them with -fPIC if(SFML_BUILD_SHARED_LIBS) - set_target_properties(ogg FLAC vorbis vorbisenc vorbisfile PROPERTIES POSITION_INDEPENDENT_CODE ON) + set_target_properties(opus ogg opusfile FLAC vorbis vorbisenc vorbisfile PROPERTIES POSITION_INDEPENDENT_CODE ON) endif() # disable building dependencies as part of a unity build, they don't support it - set_target_properties(ogg FLAC vorbis vorbisenc vorbisfile PROPERTIES UNITY_BUILD OFF) + set_target_properties(opus ogg opusfile FLAC vorbis vorbisenc vorbisfile PROPERTIES UNITY_BUILD OFF) + sfml_set_stdlib(opus) sfml_set_stdlib(ogg) + sfml_set_stdlib(opusfile) sfml_set_stdlib(FLAC) sfml_set_stdlib(vorbis) sfml_set_stdlib(vorbisenc) @@ -181,7 +207,7 @@ target_compile_definitions(sfml-audio PRIVATE SFML_IS_BIG_ENDIAN=$ #include #include +#include #include #include #include @@ -144,6 +145,7 @@ SoundFileFactory::ReaderFactoryMap& SoundFileFactory::getReaderFactoryMap() static ReaderFactoryMap result{{&priv::createReader, &priv::SoundFileReaderFlac::check}, {&priv::createReader, &priv::SoundFileReaderMp3::check}, {&priv::createReader, &priv::SoundFileReaderOgg::check}, + {&priv::createReader, &priv::SoundFileReaderOpus::check}, {&priv::createReader, &priv::SoundFileReaderWav::check}}; return result; diff --git a/src/SFML/Audio/SoundFileReaderOpus.cpp b/src/SFML/Audio/SoundFileReaderOpus.cpp new file mode 100644 index 000000000..af760d958 --- /dev/null +++ b/src/SFML/Audio/SoundFileReaderOpus.cpp @@ -0,0 +1,234 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2024 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include + +#include + +#include +#include + + +namespace +{ +int read(void* data, unsigned char* ptr, int bytes) +{ + auto* stream = static_cast(data); + return static_cast(stream->read(ptr, static_cast(bytes)).value_or(-1)); +} + +int seek(void* data, opus_int64 signedOffset, int whence) +{ + auto* stream = static_cast(data); + auto offset = static_cast(signedOffset); + switch (whence) + { + case SEEK_SET: + break; + case SEEK_CUR: + offset += stream->tell().value(); + break; + case SEEK_END: + offset = stream->getSize().value() - offset; + } + const std::optional position = stream->seek(offset); + return position ? 0 : -1; +} + +opus_int64 tell(void* data) +{ + auto* stream = static_cast(data); + const std::optional position = stream->tell(); + return position ? static_cast(*position) : -1; +} + +const OpusFileCallbacks callbacks = {&read, &seek, &tell, nullptr}; +} // namespace + +namespace sf::priv +{ +//////////////////////////////////////////////////////////// +bool SoundFileReaderOpus::check(InputStream& stream) +{ + int error = 0; + OggOpusFile* file = op_test_callbacks(&stream, &callbacks, NULL, 0, &error); + + if (error == 0) + { + op_free(file); + return true; + } + + return false; +} + + +//////////////////////////////////////////////////////////// +SoundFileReaderOpus::~SoundFileReaderOpus() +{ + close(); +} + + +//////////////////////////////////////////////////////////// +std::optional SoundFileReaderOpus::open(InputStream& stream) +{ + // Open the Opus stream + int error = 0; + m_opus = op_open_callbacks(&stream, &callbacks, nullptr, 0, &error); + if (error != 0) + { + err() << "Failed to open Opus file for reading" << std::endl; + return std::nullopt; + } + + // Retrieve the music attributes + const OpusHead* opusHead = op_head(m_opus, -1); + Info info; + info.channelCount = static_cast(opusHead->channel_count); + info.sampleCount = static_cast(op_pcm_total(m_opus, -1) * opusHead->channel_count); + + // All Opus audio is encoded at 48kHz + // https://www.opus-codec.org/docs/opusfile_api-0.12/structOpusHead.html#a73b80a913eca33d829f1667caee80d9e + info.sampleRate = 48000; + + // For Vorbis channel mapping refer to: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810004.3.9 + switch (info.channelCount) + { + case 0: + err() << "No channels in Opus file" << std::endl; + break; + case 1: + info.channelMap = {SoundChannel::Mono}; + break; + case 2: + info.channelMap = {SoundChannel::FrontLeft, SoundChannel::FrontRight}; + break; + case 3: + info.channelMap = {SoundChannel::FrontLeft, SoundChannel::FrontCenter, SoundChannel::FrontRight}; + break; + case 4: + info.channelMap = {SoundChannel::FrontLeft, SoundChannel::FrontRight, SoundChannel::BackLeft, SoundChannel::BackRight}; + break; + case 5: + info.channelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::BackLeft, + SoundChannel::BackRight}; + break; + case 6: + info.channelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::BackLeft, + SoundChannel::BackRight, + SoundChannel::LowFrequencyEffects}; + break; + case 7: + info.channelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::SideLeft, + SoundChannel::SideRight, + SoundChannel::BackCenter, + SoundChannel::LowFrequencyEffects}; + break; + case 8: + info.channelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::SideLeft, + SoundChannel::SideRight, + SoundChannel::BackLeft, + SoundChannel::BackRight, + SoundChannel::LowFrequencyEffects}; + break; + default: + err() << "Opus files with more than 8 channels not supported" << std::endl; + assert(false); + break; + } + + // We must keep the channel count for the seek function + m_channelCount = info.channelCount; + + return info; +} + + +//////////////////////////////////////////////////////////// +void SoundFileReaderOpus::seek(std::uint64_t sampleOffset) +{ + assert(m_opus && "Opus stream is missing. Call SoundFileReaderOpus::open() to initialize it."); + + op_pcm_seek(m_opus, static_cast(sampleOffset / m_channelCount)); +} + + +//////////////////////////////////////////////////////////// +std::uint64_t SoundFileReaderOpus::read(std::int16_t* samples, std::uint64_t maxCount) +{ + assert(m_opus && "Opus stream is missing. Call SoundFileReaderOpus::open() to initialize it."); + + // Try to read the requested number of samples, stop only on error or end of file + std::uint64_t count = 0; + while (count < maxCount) + { + const int bytesToRead = static_cast(maxCount - count) * static_cast(sizeof(std::int16_t)); + const long bytesRead = op_read(m_opus, static_cast(samples), bytesToRead, nullptr); + if (bytesRead > 0) + { + const long samplesRead = bytesRead / static_cast(sizeof(std::int16_t)); + count += static_cast(samplesRead); + samples += samplesRead; + } + else + { + // error or end of file + break; + } + } + + return count; +} + + +//////////////////////////////////////////////////////////// +void SoundFileReaderOpus::close() +{ + if (m_opus) + { + op_free(m_opus); + m_channelCount = 0; + } +} + +} // namespace sf::priv diff --git a/src/SFML/Audio/SoundFileReaderOpus.hpp b/src/SFML/Audio/SoundFileReaderOpus.hpp new file mode 100644 index 000000000..856e0c4d1 --- /dev/null +++ b/src/SFML/Audio/SoundFileReaderOpus.hpp @@ -0,0 +1,119 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2024 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include + +#include + +#include + + +namespace sf +{ +class InputStream; +} + +namespace sf::priv +{ +//////////////////////////////////////////////////////////// +/// \brief Implementation of sound file reader that handles Opus files +/// +//////////////////////////////////////////////////////////// +class SoundFileReaderOpus : public SoundFileReader +{ +public: + //////////////////////////////////////////////////////////// + /// \brief Check if this reader can handle a file given by an input stream + /// + /// \param stream Source stream to check + /// + /// \return `true` if the file is supported by this reader + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] static bool check(InputStream& stream); + + //////////////////////////////////////////////////////////// + /// \brief Destructor + /// + //////////////////////////////////////////////////////////// + ~SoundFileReaderOpus() override; + + //////////////////////////////////////////////////////////// + /// \brief Open a sound file for reading + /// + /// \param stream Source stream to read from + /// + /// \return Properties of the loaded sound if the file was successfully opened + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] std::optional open(InputStream& stream) override; + + //////////////////////////////////////////////////////////// + /// \brief Change the current read position to the given sample offset + /// + /// The sample offset takes the channels into account. + /// If you have a time offset instead, you can easily find + /// the corresponding sample offset with the following formula: + /// `timeInSeconds * sampleRate * channelCount` + /// If the given offset exceeds to total number of samples, + /// this function must jump to the end of the file. + /// + /// \param sampleOffset Index of the sample to jump to, relative to the beginning + /// + //////////////////////////////////////////////////////////// + void seek(std::uint64_t sampleOffset) override; + + //////////////////////////////////////////////////////////// + /// \brief Read audio samples from the open file + /// + /// \param samples Pointer to the sample array to fill + /// \param maxCount Maximum number of samples to read + /// + /// \return Number of samples actually read (may be less than \a maxCount) + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] std::uint64_t read(std::int16_t* samples, std::uint64_t maxCount) override; + +private: + //////////////////////////////////////////////////////////// + /// \brief Close the open Opus file + /// + //////////////////////////////////////////////////////////// + void close(); + + //////////////////////////////////////////////////////////// + // Member data + //////////////////////////////////////////////////////////// + OggOpusFile* m_opus; // opus file handle + unsigned int m_channelCount{}; // number of channels of the open sound file +}; + +} // namespace sf::priv diff --git a/src/SFML/Audio/SoundFileWriterOgg.cpp b/src/SFML/Audio/SoundFileWriterOgg.cpp index 9b86096ba..8c5792fed 100644 --- a/src/SFML/Audio/SoundFileWriterOgg.cpp +++ b/src/SFML/Audio/SoundFileWriterOgg.cpp @@ -118,7 +118,7 @@ bool SoundFileWriterOgg::open(const std::filesystem::path& filename, return false; } - // Check if the channel map contains channels that we cannot remap to a mapping supported by FLAC + // Check if the channel map contains channels that we cannot remap to a mapping supported by Vorbis if (!std::is_permutation(channelMap.begin(), channelMap.end(), targetChannelMap.begin())) { err() << "Provided channel map cannot be reordered to a channel map supported by Vorbis" << std::endl; diff --git a/src/SFML/Audio/SoundFileWriterOpus.cpp b/src/SFML/Audio/SoundFileWriterOpus.cpp new file mode 100644 index 000000000..834771137 --- /dev/null +++ b/src/SFML/Audio/SoundFileWriterOpus.cpp @@ -0,0 +1,328 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2024 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include + +#include +#include +#include + +#include +#include + + +namespace +{ +// Make sure to write int into buffer little endian +void writeUint32(std::vector& buffer, const std::uint32_t value) +{ + buffer.push_back(static_cast(value & 0x000000FF)); + buffer.push_back(static_cast((value & 0x0000FF00) >> 8)); + buffer.push_back(static_cast((value & 0x00FF0000) >> 16)); + buffer.push_back(static_cast((value & 0xFF000000) >> 24)); +} +} // namespace + +namespace sf::priv +{ +//////////////////////////////////////////////////////////// +bool SoundFileWriterOpus::check(const std::filesystem::path& filename) +{ + return toLower(filename.extension().string()) == ".opus"; +} + + +//////////////////////////////////////////////////////////// +SoundFileWriterOpus::~SoundFileWriterOpus() +{ + close(); +} + + +//////////////////////////////////////////////////////////// +bool SoundFileWriterOpus::open(const std::filesystem::path& filename, + unsigned int sampleRate, + unsigned int channelCount, + const std::vector& channelMap) +{ + std::vector targetChannelMap; + + // For Vorbis channel mapping refer to: https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810004.3.9 + switch (channelCount) + { + case 0: + err() << "No channels to write to Opus file" << std::endl; + return false; + case 1: + targetChannelMap = {SoundChannel::Mono}; + break; + case 2: + targetChannelMap = {SoundChannel::FrontLeft, SoundChannel::FrontRight}; + break; + case 3: + targetChannelMap = {SoundChannel::FrontLeft, SoundChannel::FrontCenter, SoundChannel::FrontRight}; + break; + case 4: + targetChannelMap = {SoundChannel::FrontLeft, SoundChannel::FrontRight, SoundChannel::BackLeft, SoundChannel::BackRight}; + break; + case 5: + targetChannelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::BackLeft, + SoundChannel::BackRight}; + break; + case 6: + targetChannelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::BackLeft, + SoundChannel::BackRight, + SoundChannel::LowFrequencyEffects}; + break; + case 7: + targetChannelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::SideLeft, + SoundChannel::SideRight, + SoundChannel::BackCenter, + SoundChannel::LowFrequencyEffects}; + break; + case 8: + targetChannelMap = {SoundChannel::FrontLeft, + SoundChannel::FrontCenter, + SoundChannel::FrontRight, + SoundChannel::SideLeft, + SoundChannel::SideRight, + SoundChannel::BackLeft, + SoundChannel::BackRight, + SoundChannel::LowFrequencyEffects}; + break; + default: + err() << "Opus files with more than 8 channels not supported" << std::endl; + return false; + } + + // Check if the channel map contains channels that we cannot remap to a mapping supported by Opus + if (!std::is_permutation(channelMap.begin(), channelMap.end(), targetChannelMap.begin())) + { + err() << "Provided channel map cannot be reordered to a channel map supported by Opus" << std::endl; + return false; + } + + // Build the remap table + for (auto i = 0u; i < channelCount; ++i) + m_remapTable[i] = static_cast( + std::find(channelMap.begin(), channelMap.end(), targetChannelMap[i]) - channelMap.begin()); + + // Save the channel count + m_channelCount = channelCount; + m_sampleRate = sampleRate; + + // Initialize the ogg/opus stream + static std::mt19937 rng(std::random_device{}()); + if (ogg_stream_init(&m_ogg, std::uniform_int_distribution(0, std::numeric_limits::max())(rng)) == -1) + { + err() << "Stream init of ogg/opus failed" << std::endl; + close(); + return false; + } + + int status = OPUS_INTERNAL_ERROR; + m_opus = opus_encoder_create(static_cast(sampleRate), static_cast(channelCount), OPUS_APPLICATION_AUDIO, &status); + if (status != OPUS_OK) + { + err() << "Failed to write ogg/opus file\n" << formatDebugPathInfo(filename) << std::endl; + if (status == OPUS_BAD_ARG) + err() << "Possibly wrong sample rate, allowed are 8000, 12000, 16000, 24000, or 48000 Hz." << std::endl; + close(); + return false; + } + + // Open the file after the opus setup is ok + m_file.open(filename, std::ios::binary); + if (!m_file) + { + err() << "Failed to write opus file (cannot open file)\n" << formatDebugPathInfo(filename) << std::endl; + close(); + return false; + } + + // Set bitrate (VBR is default) + opus_encoder_ctl(m_opus, OPUS_SET_BITRATE(128000)); + + // Create opus header MAGICBYTES + std::vector headerData({'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}); + + headerData.push_back(1); // Version + headerData.push_back(static_cast(channelCount)); + headerData.push_back(0); // Preskip + headerData.push_back(0); + + writeUint32(headerData, static_cast(sampleRate)); + + headerData.push_back(0); // Gain + headerData.push_back(0); + + headerData.push_back(channelCount > 8 ? 255 : (channelCount > 2)); // Mapping family + + // Map opus header to ogg packet + ogg_packet op; + op.packet = headerData.data(); + op.bytes = static_cast(headerData.size()); + op.b_o_s = 1; + op.e_o_s = 0; + op.granulepos = 0; + op.packetno = static_cast(m_packageNumber++); + + // Write the header packet to the ogg stream + ogg_stream_packetin(&m_ogg, &op); + flushBlocks(); + + // Create comment header, needs to be in a new page + // commentData initialized with magic bytes + std::vector commentData({'O', 'p', 'u', 's', 'T', 'a', 'g', 's'}); + + // Vendor string + const std::string opusVersion(opus_get_version_string()); + + // unsigned 32bit integer: Length of vendor string (encoding library) + writeUint32(commentData, static_cast(opusVersion.size())); + commentData.insert(commentData.end(), opusVersion.begin(), opusVersion.end()); + + // Length of user comments (E.g. one could add an ENCODER tag for SFML) + writeUint32(commentData, 0); + + op.packet = &commentData.front(); + op.bytes = static_cast(commentData.size()); + op.b_o_s = 0; + op.e_o_s = 0; + op.granulepos = 0; + op.packetno = static_cast(m_packageNumber++); + ogg_stream_packetin(&m_ogg, &op); + + // This ensures the actual audio data will start on a new page, as per spec + flushBlocks(); + + return true; +} + + +//////////////////////////////////////////////////////////// +void SoundFileWriterOpus::write(const std::int16_t* samples, std::uint64_t count) +{ + assert(m_opus && "Opus stream is missing. Call SoundFileWriterOpus::open() to initialize it."); + + const opus_uint32 frameSize = 960; + std::vector buffer(frameSize * m_channelCount); + + std::uint32_t frameNumber = 0; + std::uint8_t endOfStream = 0; + + while (count > 0) + { + opus_int32 packetSize; + + // Check if wee need to pad the input + if (count < frameSize * m_channelCount) + { + const std::uint32_t begin = frameNumber * frameSize * m_channelCount; + std::vector pad(samples + begin, samples + begin + count); + pad.insert(pad.end(), (frameSize * m_channelCount) - pad.size(),0); + packetSize = opus_encode(m_opus, &pad.front(), frameSize, buffer.data(), static_cast(buffer.size())); + endOfStream = 1; + count = 0; + } + else + { + packetSize = opus_encode(m_opus, + samples + (frameNumber * frameSize * m_channelCount), + frameSize, + &buffer.front(), + static_cast(buffer.size())); + count -= frameSize * m_channelCount; + } + + if (packetSize < 0) + { + err() << "An error occurred when encoding sound to opus." << std::endl; + break; + } + + ogg_packet op; + op.packet = &buffer.front(); + op.bytes = packetSize; + op.granulepos = frameNumber * frameSize * 48000ul / m_sampleRate; + op.packetno = static_cast(m_packageNumber++); + op.b_o_s = 0; + op.e_o_s = endOfStream; + ogg_stream_packetin(&m_ogg, &op); + + frameNumber++; + } + + // Flush any produced block + flushBlocks(); +} + + +//////////////////////////////////////////////////////////// +void SoundFileWriterOpus::flushBlocks() +{ + ogg_page page; + while ((ogg_stream_pageout(&m_ogg, &page) > 0) || (ogg_stream_flush(&m_ogg, &page) > 0)) + { + m_file.write(reinterpret_cast(page.header), page.header_len); + m_file.write(reinterpret_cast(page.body), page.body_len); + } +} + + +//////////////////////////////////////////////////////////// +void SoundFileWriterOpus::close() +{ + if (m_file.is_open()) + { + flushBlocks(); + m_file.close(); + } + + // Clear all the ogg/opus structures + ogg_stream_clear(&m_ogg); + + if (m_opus != NULL) + { + opus_encoder_destroy(m_opus); + m_opus = NULL; + } +} + +} // namespace sf::priv diff --git a/src/SFML/Audio/SoundFileWriterOpus.hpp b/src/SFML/Audio/SoundFileWriterOpus.hpp new file mode 100644 index 000000000..dcd68b5e6 --- /dev/null +++ b/src/SFML/Audio/SoundFileWriterOpus.hpp @@ -0,0 +1,117 @@ +//////////////////////////////////////////////////////////// +// +// SFML - Simple and Fast Multimedia Library +// Copyright (C) 2007-2024 Laurent Gomila (laurent@sfml-dev.org) +// +// This software is provided 'as-is', without any express or implied warranty. +// In no event will the authors be held liable for any damages arising from the use of this software. +// +// Permission is granted to anyone to use this software for any purpose, +// including commercial applications, and to alter it and redistribute it freely, +// subject to the following restrictions: +// +// 1. The origin of this software must not be misrepresented; +// you must not claim that you wrote the original software. +// If you use this software in a product, an acknowledgment +// in the product documentation would be appreciated but is not required. +// +// 2. Altered source versions must be plainly marked as such, +// and must not be misrepresented as being the original software. +// +// 3. This notice may not be removed or altered from any source distribution. +// +//////////////////////////////////////////////////////////// + +#pragma once + +//////////////////////////////////////////////////////////// +// Headers +//////////////////////////////////////////////////////////// +#include + +#include +#include + +#include +#include +#include + +#include + + +namespace sf::priv +{ +//////////////////////////////////////////////////////////// +/// \brief Implementation of sound file writer that handles Opus files +/// +//////////////////////////////////////////////////////////// +class SoundFileWriterOpus : public SoundFileWriter +{ +public: + //////////////////////////////////////////////////////////// + /// \brief Check if this writer can handle a file on disk + /// + /// \param filename Path of the sound file to check + /// + /// \return `true` if the file can be written by this writer + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] static bool check(const std::filesystem::path& filename); + + //////////////////////////////////////////////////////////// + /// \brief Destructor + /// + //////////////////////////////////////////////////////////// + ~SoundFileWriterOpus() override; + + //////////////////////////////////////////////////////////// + /// \brief Open a sound file for writing + /// + /// \param filename Path of the file to open + /// \param sampleRate Sample rate of the sound + /// \param channelCount Number of channels of the sound + /// \param channelMap Map of position in sample frame to sound channel + /// + /// \return `true` if the file was successfully opened + /// + //////////////////////////////////////////////////////////// + [[nodiscard]] bool open(const std::filesystem::path& filename, + unsigned int sampleRate, + unsigned int channelCount, + const std::vector& channelMap) override; + + //////////////////////////////////////////////////////////// + /// \brief Write audio samples to the open file + /// + /// \param samples Pointer to the sample array to write + /// \param count Number of samples to write + /// + //////////////////////////////////////////////////////////// + void write(const std::int16_t* samples, std::uint64_t count) override; + +private: + //////////////////////////////////////////////////////////// + /// \brief Flush blocks produced by the ogg stream, if any + /// + //////////////////////////////////////////////////////////// + void flushBlocks(); + + //////////////////////////////////////////////////////////// + /// \brief Close the file + /// + //////////////////////////////////////////////////////////// + void close(); + + //////////////////////////////////////////////////////////// + // Member data + //////////////////////////////////////////////////////////// + unsigned int m_channelCount{}; //!< Channel count of the sound being written + std::array m_remapTable{}; //!< Table we use to remap source to target channel order + std::ofstream m_file; //!< Output file + ogg_stream_state m_ogg{}; //!< OGG stream + OpusEncoder* m_opus{}; //!< Opus handle + std::uint64_t m_packageNumber{}; //!< + std::uint32_t m_sampleRate{}; //!< +}; + +} // namespace sf::priv diff --git a/tools/opus/PatchOpus.cmake b/tools/opus/PatchOpus.cmake new file mode 100644 index 000000000..95697f62f --- /dev/null +++ b/tools/opus/PatchOpus.cmake @@ -0,0 +1,5 @@ +file(READ "${OPUS_DIR}/CMakeLists.txt" OPUS_CMAKELISTS_CONTENTS) +string(REPLACE "feature_summary(WHAT ALL)" "" OPUS_CMAKELISTS_CONTENTS "${OPUS_CMAKELISTS_CONTENTS}") +string(REPLACE "\n\nadd_library(opus" "\nset(CMAKE_DEBUG_POSTFIX d)\nadd_library(opus" OPUS_CMAKELISTS_CONTENTS "${OPUS_CMAKELISTS_CONTENTS}") +string(REPLACE "PUBLIC_HEADER\n \"\${Opus_PUBLIC_HEADER}\"" "" OPUS_CMAKELISTS_CONTENTS "${OPUS_CMAKELISTS_CONTENTS}") +file(WRITE "${OPUS_DIR}/CMakeLists.txt" "${OPUS_CMAKELISTS_CONTENTS}") diff --git a/tools/opus/PatchOpusfile.cmake b/tools/opus/PatchOpusfile.cmake new file mode 100644 index 000000000..75d2e3e23 --- /dev/null +++ b/tools/opus/PatchOpusfile.cmake @@ -0,0 +1,4 @@ +file(READ "${OPUSFILE_DIR}/CMakeLists.txt" OPUSFILE_CMAKELISTS_CONTENTS) +string(REPLACE "\n\nadd_library(opusfile" "\nset(CMAKE_DEBUG_POSTFIX d)\nadd_library(opusfile" OPUSFILE_CMAKELISTS_CONTENTS "${OPUSFILE_CMAKELISTS_CONTENTS}") +string(REPLACE "PUBLIC_HEADER \"\${CMAKE_CURRENT_SOURCE_DIR}/include/opusfile.h\"" "" OPUSFILE_CMAKELISTS_CONTENTS "${OPUSFILE_CMAKELISTS_CONTENTS}") +file(WRITE "${OPUSFILE_DIR}/CMakeLists.txt" "${OPUSFILE_CMAKELISTS_CONTENTS}")