Add support for opus and opusfile

This commit is contained in:
Lukas Dürrenberger 2025-01-02 02:02:09 +01:00
parent bc268fbaea
commit 3d3d60a4a2
14 changed files with 853 additions and 10 deletions

View File

@ -128,7 +128,7 @@ jobs:
run: | run: |
CLANG_VERSION=$(clang++ --version | sed -n 's/.*version \([0-9]\+\)\..*/\1/p') CLANG_VERSION=$(clang++ --version | sed -n 's/.*version \([0-9]\+\)\..*/\1/p')
echo "CLANG_VERSION=$CLANG_VERSION" >> $GITHUB_ENV 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 - name: Remove ALSA Library
if: runner.os == 'Linux' && matrix.platform.name != 'Android' if: runner.os == 'Linux' && matrix.platform.name != 'Android'

View File

@ -85,6 +85,9 @@ int main()
// Play a sound // Play a sound
playSound(); playSound();
// Play music from an opus file
playMusic("error.opus");
// Play music from an ogg file // Play music from an ogg file
playMusic("doodle_pop.ogg"); playMusic("doodle_pop.ogg");

Binary file not shown.

View File

@ -108,7 +108,8 @@ public:
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
/// \brief Open a sound file from the disk for reading /// \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. /// 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, /// 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 /// \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. /// The supported sample sizes for FLAC and WAV are 8, 16, 24 and 32 bit.
/// ///
/// \param data Pointer to the file data in memory /// \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 /// \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. /// The supported sample sizes for FLAC and WAV are 8, 16, 24 and 32 bit.
/// ///
/// \param stream Source stream to read from /// \param stream Source stream to read from

View File

@ -48,6 +48,8 @@ set(CODECS_SRC
${SRCROOT}/SoundFileReaderMp3.cpp ${SRCROOT}/SoundFileReaderMp3.cpp
${SRCROOT}/SoundFileReaderOgg.hpp ${SRCROOT}/SoundFileReaderOgg.hpp
${SRCROOT}/SoundFileReaderOgg.cpp ${SRCROOT}/SoundFileReaderOgg.cpp
${SRCROOT}/SoundFileReaderOpus.hpp
${SRCROOT}/SoundFileReaderOpus.cpp
${SRCROOT}/SoundFileReaderWav.hpp ${SRCROOT}/SoundFileReaderWav.hpp
${SRCROOT}/SoundFileReaderWav.cpp ${SRCROOT}/SoundFileReaderWav.cpp
${INCROOT}/SoundFileWriter.hpp ${INCROOT}/SoundFileWriter.hpp
@ -55,6 +57,8 @@ set(CODECS_SRC
${SRCROOT}/SoundFileWriterFlac.cpp ${SRCROOT}/SoundFileWriterFlac.cpp
${SRCROOT}/SoundFileWriterOgg.hpp ${SRCROOT}/SoundFileWriterOgg.hpp
${SRCROOT}/SoundFileWriterOgg.cpp ${SRCROOT}/SoundFileWriterOgg.cpp
${SRCROOT}/SoundFileWriterOpus.hpp
${SRCROOT}/SoundFileWriterOpus.cpp
${SRCROOT}/SoundFileWriterWav.hpp ${SRCROOT}/SoundFileWriterWav.hpp
${SRCROOT}/SoundFileWriterWav.cpp ${SRCROOT}/SoundFileWriterWav.cpp
) )
@ -94,10 +98,23 @@ else()
set(INSTALL_DOCS OFF) set(INSTALL_DOCS OFF)
set(INSTALL_PKG_CONFIG_MODULE OFF) set(INSTALL_PKG_CONFIG_MODULE OFF)
set(INSTALL_PKGCONFIG_MODULES OFF) set(INSTALL_PKGCONFIG_MODULES OFF)
set(OPUS_INSTALL_PKG_CONFIG_MODULE OFF)
set(WITH_FORTIFY_SOURCE OFF) set(WITH_FORTIFY_SOURCE OFF)
set(WITH_STACK_PROTECTOR OFF) set(WITH_STACK_PROTECTOR OFF)
set(WITH_AVX OFF) # LLVM/Clang on Windows has issues with AVX2 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 FetchContent_Declare(ogg
GIT_REPOSITORY https://github.com/xiph/ogg.git GIT_REPOSITORY https://github.com/xiph/ogg.git
GIT_TAG v1.3.5 GIT_TAG v1.3.5
@ -106,6 +123,13 @@ else()
# - installing headers & pkgconfig files # - installing headers & pkgconfig files
# - add CMAKE_DEBUG_POSTFIX # - add CMAKE_DEBUG_POSTFIX
PATCH_COMMAND ${CMAKE_COMMAND} -DOGG_DIR=${FETCHCONTENT_BASE_DIR}/ogg-src -P ${PROJECT_SOURCE_DIR}/tools/ogg/PatchOgg.cmake) 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 FetchContent_Declare(flac
GIT_REPOSITORY https://github.com/xiph/flac.git GIT_REPOSITORY https://github.com/xiph/flac.git
GIT_TAG 1.4.3 GIT_TAG 1.4.3
@ -124,20 +148,22 @@ else()
# - installing headers & pkgconfig files # - installing headers & pkgconfig files
# - add CMAKE_DEBUG_POSTFIX # - add CMAKE_DEBUG_POSTFIX
PATCH_COMMAND ${CMAKE_COMMAND} -DVORBIS_DIR=${FETCHCONTENT_BASE_DIR}/vorbis-src -P ${PROJECT_SOURCE_DIR}/tools/vorbis/PatchVorbis.cmake) 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 # if building SFML as a shared library and linking our dependencies in
# as static libraries we need to build them with -fPIC # as static libraries we need to build them with -fPIC
if(SFML_BUILD_SHARED_LIBS) 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() endif()
# disable building dependencies as part of a unity build, they don't support it # 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(ogg)
sfml_set_stdlib(opusfile)
sfml_set_stdlib(FLAC) sfml_set_stdlib(FLAC)
sfml_set_stdlib(vorbis) sfml_set_stdlib(vorbis)
sfml_set_stdlib(vorbisenc) sfml_set_stdlib(vorbisenc)
@ -181,7 +207,7 @@ target_compile_definitions(sfml-audio PRIVATE SFML_IS_BIG_ENDIAN=$<STREQUAL:${CM
# setup dependencies # setup dependencies
target_link_libraries(sfml-audio target_link_libraries(sfml-audio
PUBLIC SFML::System PUBLIC SFML::System
PRIVATE Vorbis::vorbis Vorbis::vorbisfile Vorbis::vorbisenc FLAC::FLAC Threads::Threads) PRIVATE Vorbis::vorbis Vorbis::vorbisfile Vorbis::vorbisenc FLAC::FLAC OpusFile::opusfile Threads::Threads)
if(SFML_OS_IOS) if(SFML_OS_IOS)
target_link_libraries(sfml-audio PRIVATE "-framework Foundation" "-framework CoreFoundation" "-framework CoreAudio" "-framework AudioToolbox" "-framework AVFoundation") target_link_libraries(sfml-audio PRIVATE "-framework Foundation" "-framework CoreFoundation" "-framework CoreAudio" "-framework AudioToolbox" "-framework AVFoundation")
endif() endif()

View File

@ -30,9 +30,11 @@ set(FIND_SFML_DEPENDENCIES_NOTFOUND)
if(SFML_BUILT_USING_SYSTEM_DEPS) if(SFML_BUILT_USING_SYSTEM_DEPS)
find_dependency(Vorbis) find_dependency(Vorbis)
find_dependency(FLAC) find_dependency(FLAC)
find_dependency(opus)
else() else()
find_dependency(Vorbis CONFIG PATHS "${CMAKE_CURRENT_LIST_DIR}/../../../") find_dependency(Vorbis CONFIG PATHS "${CMAKE_CURRENT_LIST_DIR}/../../../")
find_dependency(FLAC CONFIG PATHS "${CMAKE_CURRENT_LIST_DIR}/../../../") find_dependency(FLAC CONFIG PATHS "${CMAKE_CURRENT_LIST_DIR}/../../../")
find_dependency(opus CONFIG PATHS "${CMAKE_CURRENT_LIST_DIR}/../../../")
endif() endif()
if(FIND_SFML_DEPENDENCIES_NOTFOUND) if(FIND_SFML_DEPENDENCIES_NOTFOUND)

View File

@ -29,6 +29,7 @@
#include <SFML/Audio/SoundFileReaderFlac.hpp> #include <SFML/Audio/SoundFileReaderFlac.hpp>
#include <SFML/Audio/SoundFileReaderMp3.hpp> #include <SFML/Audio/SoundFileReaderMp3.hpp>
#include <SFML/Audio/SoundFileReaderOgg.hpp> #include <SFML/Audio/SoundFileReaderOgg.hpp>
#include <SFML/Audio/SoundFileReaderOpus.hpp>
#include <SFML/Audio/SoundFileReaderWav.hpp> #include <SFML/Audio/SoundFileReaderWav.hpp>
#include <SFML/Audio/SoundFileWriterFlac.hpp> #include <SFML/Audio/SoundFileWriterFlac.hpp>
#include <SFML/Audio/SoundFileWriterOgg.hpp> #include <SFML/Audio/SoundFileWriterOgg.hpp>
@ -144,6 +145,7 @@ SoundFileFactory::ReaderFactoryMap& SoundFileFactory::getReaderFactoryMap()
static ReaderFactoryMap result{{&priv::createReader<priv::SoundFileReaderFlac>, &priv::SoundFileReaderFlac::check}, static ReaderFactoryMap result{{&priv::createReader<priv::SoundFileReaderFlac>, &priv::SoundFileReaderFlac::check},
{&priv::createReader<priv::SoundFileReaderMp3>, &priv::SoundFileReaderMp3::check}, {&priv::createReader<priv::SoundFileReaderMp3>, &priv::SoundFileReaderMp3::check},
{&priv::createReader<priv::SoundFileReaderOgg>, &priv::SoundFileReaderOgg::check}, {&priv::createReader<priv::SoundFileReaderOgg>, &priv::SoundFileReaderOgg::check},
{&priv::createReader<priv::SoundFileReaderOpus>, &priv::SoundFileReaderOpus::check},
{&priv::createReader<priv::SoundFileReaderWav>, &priv::SoundFileReaderWav::check}}; {&priv::createReader<priv::SoundFileReaderWav>, &priv::SoundFileReaderWav::check}};
return result; return result;

View File

@ -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 <SFML/Audio/SoundFileReaderOpus.hpp>
#include <SFML/System/Err.hpp>
#include <SFML/System/InputStream.hpp>
#include <ostream>
#include <cassert>
#include <cstdio>
namespace
{
int read(void* data, unsigned char* ptr, int bytes)
{
auto* stream = static_cast<sf::InputStream*>(data);
return static_cast<int>(stream->read(ptr, static_cast<std::size_t>(bytes)).value_or(-1));
}
int seek(void* data, opus_int64 signedOffset, int whence)
{
auto* stream = static_cast<sf::InputStream*>(data);
auto offset = static_cast<std::size_t>(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<sf::InputStream*>(data);
const std::optional position = stream->tell();
return position ? static_cast<long>(*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<SoundFileReader::Info> 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<unsigned int>(opusHead->channel_count);
info.sampleCount = static_cast<std::size_t>(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<ogg_int64_t>(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<int>(maxCount - count) * static_cast<int>(sizeof(std::int16_t));
const long bytesRead = op_read(m_opus, static_cast<opus_int16*>(samples), bytesToRead, nullptr);
if (bytesRead > 0)
{
const long samplesRead = bytesRead / static_cast<long>(sizeof(std::int16_t));
count += static_cast<std::uint64_t>(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

View File

@ -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 <SFML/Audio/SoundFileReader.hpp>
#include <opusfile.h>
#include <optional>
#include <cstdint>
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<Info> 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

View File

@ -118,7 +118,7 @@ bool SoundFileWriterOgg::open(const std::filesystem::path& filename,
return false; 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())) 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; err() << "Provided channel map cannot be reordered to a channel map supported by Vorbis" << std::endl;

View File

@ -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 <SFML/Audio/SoundFileWriterOpus.hpp>
#include <SFML/System/Err.hpp>
#include <SFML/System/Utils.hpp>
#include <algorithm>
#include <ostream>
#include <random>
#include <cassert>
#include <cstdint>
namespace
{
// Make sure to write int into buffer little endian
void writeUint32(std::vector<unsigned char>& buffer, const std::uint32_t value)
{
buffer.push_back(static_cast<unsigned char>(value & 0x000000FF));
buffer.push_back(static_cast<unsigned char>((value & 0x0000FF00) >> 8));
buffer.push_back(static_cast<unsigned char>((value & 0x00FF0000) >> 16));
buffer.push_back(static_cast<unsigned char>((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<SoundChannel>& channelMap)
{
std::vector<SoundChannel> 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::size_t>(
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<int>::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<opus_int32>(sampleRate), static_cast<int>(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<unsigned char> headerData({'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'});
headerData.push_back(1); // Version
headerData.push_back(static_cast<unsigned char>(channelCount));
headerData.push_back(0); // Preskip
headerData.push_back(0);
writeUint32(headerData, static_cast<std::uint32_t>(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<long>(headerData.size());
op.b_o_s = 1;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = static_cast<ogg_int64_t>(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<unsigned char> 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<std::uint32_t>(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<long>(commentData.size());
op.b_o_s = 0;
op.e_o_s = 0;
op.granulepos = 0;
op.packetno = static_cast<ogg_int64_t>(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<unsigned char> 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<opus_int16> 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<opus_int32>(buffer.size()));
endOfStream = 1;
count = 0;
}
else
{
packetSize = opus_encode(m_opus,
samples + (frameNumber * frameSize * m_channelCount),
frameSize,
&buffer.front(),
static_cast<opus_int32>(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<ogg_int64_t>(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<const char*>(page.header), page.header_len);
m_file.write(reinterpret_cast<const char*>(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

View File

@ -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 <SFML/Audio/SoundFileWriter.hpp>
#include <opus.h>
#include <ogg/ogg.h>
#include <array>
#include <filesystem>
#include <fstream>
#include <cstdint>
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<SoundChannel>& 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<std::size_t, 8> 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

View File

@ -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}")

View File

@ -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}")