diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 1abd005d06131..f81c70f9eab49 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -63,6 +63,7 @@ jobs: libogg-dev \ libopus-dev \ libopusfile-dev \ + libsoxr-dev \ ${{ inputs.libraries }} \ zlib1g-dev \ # EOF diff --git a/.github/workflows/ci-mingw.yml b/.github/workflows/ci-mingw.yml index b1079a534ca01..db35cb16d67f8 100644 --- a/.github/workflows/ci-mingw.yml +++ b/.github/workflows/ci-mingw.yml @@ -40,6 +40,7 @@ jobs: mingw-w64-${{ inputs.arch }}-libogg mingw-w64-${{ inputs.arch }}-opus mingw-w64-${{ inputs.arch }}-opusfile + mingw-w64-${{ inputs.arch }}-libsoxr - name: Install OpenGFX shell: bash diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index b1b9a904e4d84..caab5e7f79212 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -57,6 +57,7 @@ jobs: libopus-dev \ libopusfile-dev \ libsdl2-dev \ + libsoxr-dev \ zlib1g-dev \ # EOF diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bf26bd03ff5e..89e162a2559b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -156,6 +156,7 @@ if(NOT OPTION_DEDICATED) endif() endif() find_package(OpusFile) + find_package(Soxr) endif() if(APPLE) enable_language(OBJCXX) @@ -334,6 +335,7 @@ if(NOT OPTION_DEDICATED) link_package(ICU_i18n) link_package(ICU_uc) link_package(OpusFile TARGET OpusFile::opusfile) + link_package(Soxr) 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/FindSoxr.cmake b/cmake/FindSoxr.cmake new file mode 100644 index 0000000000000..13b8988a65499 --- /dev/null +++ b/cmake/FindSoxr.cmake @@ -0,0 +1,32 @@ +include(FindPackageHandleStandardArgs) + +find_library(Soxr_LIBRARY + NAMES soxr +) + +set(Soxr_COMPILE_OPTIONS "" CACHE STRING "Extra compile options of soxr") + +set(Soxr_LINK_LIBRARIES "" CACHE STRING "Extra link libraries of soxr") + +set(Soxr_LINK_FLAGS "" CACHE STRING "Extra link flags of soxr") + +find_path(Soxr_INCLUDE_PATH + NAMES soxr.h +) + +find_package_handle_standard_args(Soxr + REQUIRED_VARS Soxr_LIBRARY Soxr_INCLUDE_PATH +) + +if(Soxr_FOUND) + if(NOT TARGET Soxr::soxr) + add_library(Soxr::soxr UNKNOWN IMPORTED) + set_target_properties(Soxr::soxr PROPERTIES + IMPORTED_LOCATION "${Soxr_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${Soxr_INCLUDE_PATH}" + INTERFACE_COMPILE_OPTIONS "${Soxr_COMPILE_OPTIONS}" + INTERFACE_LINK_LIBRARIES "${Soxr_LINK_LIBRARIES}" + INTERFACE_LINK_FLAGS "${Soxr_LINK_FLAGS}" + ) + endif() +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 599d45aa5e0be..c18e67e0e9b68 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -44,6 +44,11 @@ add_files( CONDITION OpusFile_FOUND ) +add_files( + soundresampler_soxr.cpp + CONDITION Soxr_FOUND +) + add_files( aircraft.h aircraft_cmd.cpp diff --git a/src/mixer.cpp b/src/mixer.cpp index 81205cf398d77..3fa1858386b18 100644 --- a/src/mixer.cpp +++ b/src/mixer.cpp @@ -248,6 +248,11 @@ bool MxInitialize(uint rate) return true; } +uint32_t MxGetRate() +{ + return _play_rate; +} + void SetEffectVolume(uint8_t volume) { _effect_vol.store(volume, std::memory_order_relaxed); diff --git a/src/mixer.h b/src/mixer.h index 265725696e702..ac370cfbd65aa 100644 --- a/src/mixer.h +++ b/src/mixer.h @@ -21,6 +21,7 @@ struct MixerChannel; typedef void(*MxStreamCallback)(int16_t *buffer, size_t samples); bool MxInitialize(uint rate); +uint32_t MxGetRate(); void MxMixSamples(void *buffer, uint samples); MixerChannel *MxAllocateChannel(); diff --git a/src/soundloader.cpp b/src/soundloader.cpp index 4503392cbfb8d..3bd977d76bea5 100644 --- a/src/soundloader.cpp +++ b/src/soundloader.cpp @@ -13,12 +13,14 @@ #include "soundloader_type.h" #include "soundloader_func.h" #include "string_func.h" +#include "mixer.h" #include "newgrf_sound.h" #include "random_access_file_type.h" #include "safeguards.h" template class ProviderManager; +template class ProviderManager; bool LoadSoundData(SoundEntry &sound, bool new_format, SoundID sound_id, const std::string &name) { @@ -43,6 +45,14 @@ bool LoadSoundData(SoundEntry &sound, bool new_format, SoundID sound_id, const s Debug(grf, 2, "LoadSound [{}]: channels {}, sample rate {}, bits per sample {}, length {}", sound.file->GetSimplifiedFilename(), sound.channels, sound.rate, sound.bits_per_sample, sound.file_size); + /* Convert sample rate if needed. */ + const uint32_t play_rate = MxGetRate(); + if (play_rate != sound.rate) { + for (auto &resampler : ProviderManager::GetProviders()) { + if (resampler->Resample(sound, play_rate)) break; + } + } + /* Mixer always requires an extra sample at the end for the built-in linear resampler. */ sound.data->resize(sound.data->size() + sound.channels * sound.bits_per_sample / 8); sound.data->shrink_to_fit(); diff --git a/src/soundloader_type.h b/src/soundloader_type.h index 978a1b94b65ab..4916abe878661 100644 --- a/src/soundloader_type.h +++ b/src/soundloader_type.h @@ -5,7 +5,7 @@ * 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 soundloader_type.h Types related to sound loaders. */ +/** @file soundloader_type.h Types related to sound loaders and resamplers. */ #ifndef SOUNDLOADER_TYPE_H #define SOUNDLOADER_TYPE_H @@ -29,4 +29,20 @@ class SoundLoader : public PriorityBaseProvider { virtual bool Load(SoundEntry &sound, bool new_format, std::vector &data) = 0; }; +/** Base interface for a SoundResampler implementation. */ +class SoundResampler : public PriorityBaseProvider { +public: + SoundResampler(std::string_view name, std::string_view description, int priority) : PriorityBaseProvider(name, description, priority) + { + ProviderManager::Register(*this); + } + + virtual ~SoundResampler() + { + ProviderManager::Unregister(*this); + } + + virtual bool Resample(SoundEntry &sound, uint32_t play_rate) = 0; +}; + #endif /* SOUNDLOADER_TYPE_H */ diff --git a/src/soundresampler_soxr.cpp b/src/soundresampler_soxr.cpp new file mode 100644 index 0000000000000..60dbf87f1240c --- /dev/null +++ b/src/soundresampler_soxr.cpp @@ -0,0 +1,91 @@ +/* + * 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 soundresampler_soxr.cpp SOXR sound resampler. */ + +#include "stdafx.h" +#include "core/math_func.hpp" +#include "debug.h" +#include "sound_type.h" +#include "soundloader_type.h" + +#include +#include + +#include "safeguards.h" + +class SoundResampler_Soxr : public SoundResampler { +public: + SoundResampler_Soxr() : SoundResampler("soxr", "SOXR sound resampler", 0) {} + + static constexpr int BITS_PER_BYTE = 8; + static constexpr int SOXR_BITS_PER_SAMPLE = 16; + + /** Convert samples from 8 bits to 16 bits. + * @param in Vector of samples to convert. + * @param out Vector to place converted samples. + * @pre out vector must be exactly twice the size of in vector. + */ + static void ConvertInt8toInt16(std::vector &in, std::vector &out) + { + assert(std::size(out) == std::size(in) * 2); + + auto out_it = std::begin(out); + for (const uint8_t &value : in) { + if constexpr (std::endian::native != std::endian::little) { + *out_it++ = value; + *out_it++ = 0; + } else { + *out_it++ = 0; + *out_it++ = value; + } + } + } + + bool Resample(SoundEntry &sound, uint32_t play_rate) override + { + /* We always convert with the same configuration, these are static so they only need to be set up once. */ + static const soxr_io_spec_t io = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I); // 16-bit input and output. + static const soxr_quality_spec_t quality = soxr_quality_spec(SOXR_VHQ, 0); // Use 'Very high quality'. + static const soxr_runtime_spec_t runtime = soxr_runtime_spec(std::thread::hardware_concurrency()); // Enable multi-threading. + + /* The sound data to work on. */ + std::vector &data = *sound.data; + + std::vector tmp; + if (sound.bits_per_sample == SOXR_BITS_PER_SAMPLE) { + /* No conversion necessary so just move from sound data to temporary buffer. */ + data.swap(tmp); + } else { + /* SoxR cannot resample 8-bit audio, so convert from 8-bit to 16-bit into temporary buffer. */ + tmp.resize(std::size(data) * sizeof(int16_t)); + ConvertInt8toInt16(data, tmp); + sound.bits_per_sample = SOXR_BITS_PER_SAMPLE; + } + + /* Resize buffer ensuring it is correctly aligned. */ + uint align = sound.channels * sound.bits_per_sample / BITS_PER_BYTE; + data.resize(Align(std::size(tmp) * play_rate / sound.rate, align)); + + soxr_error_t error = soxr_oneshot(sound.rate, play_rate, sound.channels, + std::data(tmp), std::size(tmp) / align, nullptr, + std::data(data), std::size(data) / align, nullptr, + &io, &quality, &runtime); + + if (error != nullptr) { + /* Could not resample, try using the original data as-is without resampling instead. */ + Debug(misc, 0, "Failed to resample: {}", soxr_strerror(error)); + data.swap(tmp); + } else { + sound.rate = play_rate; + } + + return true; + } +}; + +static SoundResampler_Soxr s_sound_resampler_soxr; diff --git a/vcpkg.json b/vcpkg.json index b7bb682bbf476..5ef9d5458169b 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -45,6 +45,9 @@ { "name": "opusfile" }, + { + "name": "soxr" + }, { "name": "sdl2", "platform": "linux"