diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 63bf1e18db332..1abd005d06131 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -60,6 +60,9 @@ jobs: libicu-dev \ liblzma-dev \ liblzo2-dev \ + libogg-dev \ + libopus-dev \ + libopusfile-dev \ ${{ inputs.libraries }} \ zlib1g-dev \ # EOF diff --git a/.github/workflows/ci-mingw.yml b/.github/workflows/ci-mingw.yml index 6a244ee6b4d17..f8a77dd8f73ea 100644 --- a/.github/workflows/ci-mingw.yml +++ b/.github/workflows/ci-mingw.yml @@ -37,6 +37,9 @@ jobs: mingw-w64-${{ inputs.arch }}-libpng mingw-w64-${{ inputs.arch }}-lld mingw-w64-${{ inputs.arch }}-ninja + mingw-w64-${{ inputs.arch }}-ogg + mingw-w64-${{ inputs.arch }}-opus + mingw-w64-${{ inputs.arch }}-opusfile - name: Install OpenGFX shell: bash diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4d10111a4eb1b..b1b9a904e4d84 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -54,6 +54,8 @@ jobs: libicu-dev \ liblzma-dev \ liblzo2-dev \ + libopus-dev \ + libopusfile-dev \ libsdl2-dev \ zlib1g-dev \ # EOF diff --git a/CMakeLists.txt b/CMakeLists.txt index 2f0248047506a..3bf26bd03ff5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -155,6 +155,7 @@ if(NOT OPTION_DEDICATED) find_package(ICU OPTIONAL_COMPONENTS i18n uc) endif() endif() + find_package(OpusFile) endif() if(APPLE) enable_language(OBJCXX) @@ -332,6 +333,7 @@ if(NOT OPTION_DEDICATED) link_package(Harfbuzz TARGET harfbuzz::harfbuzz) link_package(ICU_i18n) link_package(ICU_uc) + link_package(OpusFile TARGET OpusFile::opusfile) if(SDL2_FOUND AND OPENGL_FOUND AND UNIX) # SDL2 dynamically loads OpenGL if needed, so do not link to OpenGL when diff --git a/cmake/FindOgg.cmake b/cmake/FindOgg.cmake new file mode 100644 index 0000000000000..a6487fe08361f --- /dev/null +++ b/cmake/FindOgg.cmake @@ -0,0 +1,37 @@ +include(FindPackageHandleStandardArgs) + +find_library(Ogg_LIBRARY + NAMES ogg +) + +set(Ogg_COMPILE_OPTIONS "" CACHE STRING "Extra compile options of ogg") + +set(Ogg_LINK_LIBRARIES "" CACHE STRING "Extra link libraries of ogg") + +set(Ogg_LINK_FLAGS "" CACHE STRING "Extra link flags of ogg") + +find_path(Ogg_INCLUDE_PATH + NAMES ogg.h + PATH_SUFFIXES ogg +) + +find_package_handle_standard_args(Ogg + REQUIRED_VARS Ogg_LIBRARY Ogg_INCLUDE_PATH +) + +if (Ogg_FOUND) + set(Ogg_dirs ${Ogg_INCLUDE_PATH}) + if(EXISTS "${Ogg_INCLUDE_PATH}/ogg") + list(APPEND Ogg_dirs "${Ogg_INCLUDE_PATH}/ogg") + endif() + if (NOT TARGET Ogg::ogg) + add_library(Ogg::ogg UNKNOWN IMPORTED) + set_target_properties(Ogg::ogg PROPERTIES + IMPORTED_LOCATION "${Ogg_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Ogg_dirs}" + INTERFACE_COMPILE_OPTIONS "${Ogg_COMPILE_OPTIONS}" + INTERFACE_LINK_LIBRARIES "${Ogg_LINK_LIBRARIES}" + INTERFACE_LINK_FLAGS "${Ogg_LINK_FLAGS}" + ) + endif() +endif() diff --git a/cmake/FindOpus.cmake b/cmake/FindOpus.cmake new file mode 100644 index 0000000000000..c8ad1b48a5ba9 --- /dev/null +++ b/cmake/FindOpus.cmake @@ -0,0 +1,37 @@ +include(FindPackageHandleStandardArgs) + +find_library(Opus_LIBRARY + NAMES opus +) + +set(Opus_COMPILE_OPTIONS "" CACHE STRING "Extra compile options of opus") + +set(Opus_LINK_LIBRARIES "" CACHE STRING "Extra link libraries of opus") + +set(Opus_LINK_FLAGS "" CACHE STRING "Extra link flags of opus") + +find_path(Opus_INCLUDE_PATH + NAMES opus.h + PATH_SUFFIXES opus +) + +find_package_handle_standard_args(Opus + REQUIRED_VARS Opus_LIBRARY Opus_INCLUDE_PATH +) + +if (Opus_FOUND) + set(Opus_dirs ${Opus_INCLUDE_PATH}) + if(EXISTS "${Opus_INCLUDE_PATH}/opus") + list(APPEND Opus_dirs "${Opus_INCLUDE_PATH}/opus") + endif() + if (NOT TARGET Opus::opus) + add_library(Opus::opus UNKNOWN IMPORTED) + set_target_properties(Opus::opus PROPERTIES + IMPORTED_LOCATION "${Opus_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Opus_dirs}" + INTERFACE_COMPILE_OPTIONS "${Opus_COMPILE_OPTIONS}" + INTERFACE_LINK_LIBRARIES "${Opus_LINK_LIBRARIES}" + INTERFACE_LINK_FLAGS "${Opus_LINK_FLAGS}" + ) + endif() +endif() diff --git a/cmake/FindOpusFile.cmake b/cmake/FindOpusFile.cmake new file mode 100644 index 0000000000000..8cc4a3b263b5d --- /dev/null +++ b/cmake/FindOpusFile.cmake @@ -0,0 +1,40 @@ +include(FindPackageHandleStandardArgs) + +find_library(OpusFile_LIBRARY + NAMES opusfile +) + +set(OpusFile_COMPILE_OPTIONS "" CACHE STRING "Extra compile options of opusfile") + +set(OpusFile_LINK_LIBRARIES "" CACHE STRING "Extra link libraries of opusfile") + +set(OpusFile_LINK_FLAGS "" CACHE STRING "Extra link flags of opusfile") + +find_path(OpusFile_INCLUDE_PATH + NAMES opusfile.h + PATH_SUFFIXES opus +) + +find_package_handle_standard_args(OpusFile + REQUIRED_VARS OpusFile_LIBRARY OpusFile_INCLUDE_PATH +) + +find_package(Ogg) +find_package(Opus) + +if (OpusFile_FOUND) + set(OpusFile_dirs ${OpusFile_INCLUDE_PATH}) + if(EXISTS "${OpusFile_INCLUDE_PATH}/opus") + list(APPEND OpusFile_dirs "${OpusFile_INCLUDE_PATH}/opus") + endif() + if (NOT TARGET OpusFile::opusfile) + add_library(OpusFile::opusfile UNKNOWN IMPORTED) + set_target_properties(OpusFile::opusfile PROPERTIES + IMPORTED_LOCATION "${OpusFile_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${OpusFile_dirs}" + INTERFACE_COMPILE_OPTIONS "${OpusFile_COMPILE_OPTIONS}" + INTERFACE_LINK_LIBRARIES "Ogg::ogg;Opus::opus;${OpusFile_LINK_LIBRARIES}" + INTERFACE_LINK_FLAGS "${OpusFile_LINK_FLAGS}" + ) + endif() +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d99cb7df5954c..599d45aa5e0be 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,11 @@ add_files( CONDITION ICU_i18n_FOUND AND HARFBUZZ_FOUND ) +add_files( + soundloader_opus.cpp + CONDITION OpusFile_FOUND +) + add_files( aircraft.h aircraft_cmd.cpp diff --git a/src/soundloader_opus.cpp b/src/soundloader_opus.cpp new file mode 100644 index 0000000000000..72efd806e95bc --- /dev/null +++ b/src/soundloader_opus.cpp @@ -0,0 +1,95 @@ +/* + * This file is part of OpenTTD. + * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. + * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see . + */ + +/** @file sound_opus.cpp Loading of opus sounds. */ + +#include "stdafx.h" +#include "random_access_file_type.h" +#include "sound_type.h" +#include "soundloader_type.h" + +#include + +#include "safeguards.h" + +/** Opus sound loader. */ +class SoundLoader_Opus : public SoundLoader { +public: + SoundLoader_Opus() : SoundLoader("opus", "Opus sound loader", 10) {} + + static constexpr uint16_t OPUS_SAMPLE_RATE = 48000; ///< OpusFile always decodes at 48kHz. + static constexpr uint8_t OPUS_SAMPLE_BITS = 16; ///< OpusFile op_read() uses 16 bits per sample. + + /* For good results, you will need at least 57 bytes (for a pure Opus-only stream). */ + static constexpr size_t MIN_OPUS_FILE_SIZE = 57U; + + /* It is recommended that this be large enough for at least 120 ms of data at 48 kHz per channel (5760 values per channel). + * Smaller buffers will simply return less data, possibly consuming more memory to buffer the data internally. */ + static constexpr size_t DECODE_BUFFER_SAMPLES = 5760 * 2; + static constexpr size_t DECODE_BUFFER_BYTES = DECODE_BUFFER_SAMPLES * sizeof(opus_int16); + + bool Load(SoundEntry &sound, bool new_format, std::vector &data) override + { + if (!new_format) return false; + + /* At least 57 bytes are needed for an Opus-only file. */ + if (sound.file_size < MIN_OPUS_FILE_SIZE) return false; + + /* Test if data is an Ogg Opus stream, as identified by the initial file header. */ + auto filepos = sound.file->GetPos(); + std::vector tmp(MIN_OPUS_FILE_SIZE); + sound.file->ReadBlock(tmp.data(), tmp.size()); + if (op_test(nullptr, tmp.data(), tmp.size()) != 0) return false; + + /* Read the whole file into memory. */ + tmp.resize(sound.file_size); + sound.file->SeekTo(filepos, SEEK_SET); + sound.file->ReadBlock(tmp.data(), tmp.size()); + + int error = 0; + auto of = std::unique_ptr(op_open_memory(tmp.data(), tmp.size(), &error)); + if (error != 0) return false; + + size_t datapos = 0; + for (;;) { + data.resize(datapos + DECODE_BUFFER_BYTES); + + int link_index; + int read = op_read(of.get(), reinterpret_cast(&data[datapos]), DECODE_BUFFER_BYTES, &link_index); + if (read == 0) break; + + if (read < 0 || op_channel_count(of.get(), link_index) != 1) { + /* Error reading, or incorrect channel count. */ + data.clear(); + return false; + } + + datapos += read * sizeof(opus_int16); + } + + /* OpusFile always decodes at 48kHz. */ + sound.channels = 1; + sound.bits_per_sample = OPUS_SAMPLE_BITS; + sound.rate = OPUS_SAMPLE_RATE; + + /* We resized by DECODE_BUFFER_BYTES just before finally reading zero bytes, undo this. */ + data.resize(data.size() - DECODE_BUFFER_BYTES); + + return true; + } + +private: + /** Helper class to RAII release an OggOpusFile. */ + struct OggOpusFileDeleter { + void operator()(OggOpusFile *of) + { + if (of != nullptr) op_free(of); + } + }; +}; + +static SoundLoader_Opus s_sound_loader_opus; diff --git a/vcpkg.json b/vcpkg.json index 15079f7a310cd..b7bb682bbf476 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -42,6 +42,9 @@ { "name": "lzo" }, + { + "name": "opusfile" + }, { "name": "sdl2", "platform": "linux"