From 5dea17058976b6680c73b44d77c03e748696a16e Mon Sep 17 00:00:00 2001 From: Tyler Trahan Date: Mon, 15 Sep 2025 19:05:35 -0400 Subject: [PATCH 1/2] Fix #14588: Show error when unable to clone partly-cleared crashed train (#14591) --- src/vehicle_cmd.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/vehicle_cmd.cpp b/src/vehicle_cmd.cpp index 93f665d4bdc6a..c27fb92ded52a 100644 --- a/src/vehicle_cmd.cpp +++ b/src/vehicle_cmd.cpp @@ -849,7 +849,8 @@ std::tuple CmdCloneVehicle(DoCommandFlags flags, TileInd CommandCost ret = CheckOwnership(v->owner); if (ret.Failed()) return { ret, VehicleID::Invalid() }; - if (v->type == VEH_TRAIN && (!v->IsFrontEngine() || Train::From(v)->crash_anim_pos >= 4400)) return { CMD_ERROR, VehicleID::Invalid() }; + /* Crashed trains can only be cloned before cleanup begins. */ + if (v->type == VEH_TRAIN && (!v->IsFrontEngine() || Train::From(v)->crash_anim_pos >= 4400)) return { CommandCost(STR_ERROR_VEHICLE_IS_DESTROYED), VehicleID::Invalid() }; /* check that we can allocate enough vehicles */ if (!flags.Test(DoCommandFlag::Execute)) { From ab2e1ee1e966c7922590c1d5311bb120b929f775 Mon Sep 17 00:00:00 2001 From: Peter Nelson Date: Tue, 28 Nov 2023 00:57:07 +0000 Subject: [PATCH 2/2] Add: Use LibSoxr for high quality sample-rate conversion. If not present the existing bad quality linear conversion can still be used. --- .github/workflows/ci-linux.yml | 1 + .github/workflows/ci-mingw.yml | 1 + .github/workflows/codeql.yml | 1 + CMakeLists.txt | 2 + cmake/FindSoxr.cmake | 32 ++++++++++++ src/CMakeLists.txt | 6 +++ src/mixer.cpp | 5 ++ src/mixer.h | 1 + src/soundloader.cpp | 11 ++++ src/soundresampler_soxr.cpp | 91 ++++++++++++++++++++++++++++++++++ src/soundresampler_type.h | 32 ++++++++++++ vcpkg.json | 3 ++ 12 files changed, 186 insertions(+) create mode 100644 cmake/FindSoxr.cmake create mode 100644 src/soundresampler_soxr.cpp create mode 100644 src/soundresampler_type.h diff --git a/.github/workflows/ci-linux.yml b/.github/workflows/ci-linux.yml index 2883cb55d73e5..64786cb2f3134 100644 --- a/.github/workflows/ci-linux.yml +++ b/.github/workflows/ci-linux.yml @@ -56,6 +56,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 0aaaea5e00dce..0943a03472ed6 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -50,6 +50,7 @@ jobs: libopus-dev \ libopusfile-dev \ libsdl2-dev \ + libsoxr-dev \ zlib1g-dev \ # EOF diff --git a/CMakeLists.txt b/CMakeLists.txt index 60f4bc43fff9e..bd30944cdadcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,6 +152,7 @@ if(NOT OPTION_DEDICATED) endif() endif() find_package(OpusFile) + find_package(Soxr) endif() if(APPLE) enable_language(OBJCXX) @@ -326,6 +327,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 16215e7f8d867..c43a715f15db9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -50,6 +50,11 @@ add_files( CONDITION OpusFile_FOUND ) +add_files( + soundresampler_soxr.cpp + CONDITION Soxr_FOUND +) + add_files( aircraft.h aircraft_cmd.cpp @@ -448,6 +453,7 @@ add_files( soundloader_type.h soundloader_raw.cpp soundloader_wav.cpp + soundresampler_type.h source_type.h sprite.cpp sprite.h diff --git a/src/mixer.cpp b/src/mixer.cpp index ec46d786e363b..08bbd8d0b7d7d 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 707967d2346d9..15adb0b6de4e1 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 96405435fba14..f848c70c9bc56 100644 --- a/src/soundloader.cpp +++ b/src/soundloader.cpp @@ -12,13 +12,16 @@ #include "sound_type.h" #include "soundloader_type.h" #include "soundloader_func.h" +#include "soundresampler_type.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 +46,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/soundresampler_soxr.cpp b/src/soundresampler_soxr.cpp new file mode 100644 index 0000000000000..c2d380013a81c --- /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 "soundresampler_type.h" + +#include +#include + +#include "safeguards.h" + +class SoundResampler_Soxr : public SoundResampler { +public: + SoundResampler_Soxr() : SoundResampler("soxr", "SOXR sound resampler", 0) {} + + /** + * 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 std::byte &value : in) { + if constexpr (std::endian::native != std::endian::little) { + *out_it++ = value; + *out_it++ = std::byte{0}; + } else { + *out_it++ = std::byte{0}; + *out_it++ = value; + } + } + } + + bool Resample(SoundEntry &sound, uint32_t play_rate) override + { + /* The sound data to work on. */ + std::vector &data = *sound.data; + + std::vector tmp; + if (sound.bits_per_sample == 16) { + /* 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 = 16; + } + + /* Resize buffer ensuring it is correctly aligned. */ + uint align = sound.channels * sound.bits_per_sample / 8; + 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, + &this->io, &this->quality, &this->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; + } + +private: + static SoundResampler_Soxr instance; + + const soxr_io_spec_t io = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I); // 16-bit input and output. + const soxr_quality_spec_t quality = soxr_quality_spec(SOXR_VHQ, 0); // Use 'Very high quality'. + const soxr_runtime_spec_t runtime = soxr_runtime_spec(std::thread::hardware_concurrency()); // Enable multi-threading. +}; + +/* static */ SoundResampler_Soxr SoundResampler_Soxr::instance{}; diff --git a/src/soundresampler_type.h b/src/soundresampler_type.h new file mode 100644 index 0000000000000..fba0d10cfe3cc --- /dev/null +++ b/src/soundresampler_type.h @@ -0,0 +1,32 @@ +/* + * 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_type.h Types related to sound resamplers. */ + +#ifndef SOUNDRESAMPLER_TYPE_H +#define SOUNDRESAMPLER_TYPE_H + +#include "provider_manager.h" +#include "sound_type.h" + +/** 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 /* SOUNDRESAMPLER_TYPE_H */ diff --git a/vcpkg.json b/vcpkg.json index 99e1bd738a89a..9c55e473fa6e5 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -45,6 +45,9 @@ { "name": "opusfile" }, + { + "name": "soxr" + }, { "name": "sdl2", "platform": "linux"