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"