Skip to content

Commit

Permalink
Add: Use LibSoxr for high quality sample-rate conversion.
Browse files Browse the repository at this point in the history
If not present the existing bad quality linear conversion can still be used.
  • Loading branch information
PeterN committed Dec 7, 2024
1 parent f5d78f9 commit 147d663
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 1 deletion.
1 change: 1 addition & 0 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jobs:
libogg-dev \
libopus-dev \
libopusfile-dev \
libsoxr-dev \
${{ inputs.libraries }} \
zlib1g-dev \
# EOF
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/ci-mingw.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ jobs:
libopus-dev \
libopusfile-dev \
libsdl2-dev \
libsoxr-dev \
zlib1g-dev \
# EOF
Expand Down
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ if(NOT OPTION_DEDICATED)
endif()
endif()
find_package(OpusFile)
find_package(Soxr)
endif()
if(APPLE)
enable_language(OBJCXX)
Expand Down Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions cmake/FindSoxr.cmake
Original file line number Diff line number Diff line change
@@ -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()
5 changes: 5 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ add_files(
CONDITION OpusFile_FOUND
)

add_files(
soundresampler_soxr.cpp
CONDITION Soxr_FOUND
)

add_files(
aircraft.h
aircraft_cmd.cpp
Expand Down
5 changes: 5 additions & 0 deletions src/mixer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/mixer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
10 changes: 10 additions & 0 deletions src/soundloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<SoundLoader>;
template class ProviderManager<SoundResampler>;

bool LoadSoundData(SoundEntry &sound, bool new_format, SoundID sound_id, const std::string &name)
{
Expand All @@ -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<SoundResampler>::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();
Expand Down
18 changes: 17 additions & 1 deletion src/soundloader_type.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <http://www.gnu.org/licenses/>.
*/

/** @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
Expand All @@ -29,4 +29,20 @@ class SoundLoader : public PriorityBaseProvider<SoundLoader> {
virtual bool Load(SoundEntry &sound, bool new_format, std::vector<uint8_t> &data) = 0;
};

/** Base interface for a SoundResampler implementation. */
class SoundResampler : public PriorityBaseProvider<SoundResampler> {
public:
SoundResampler(std::string_view name, std::string_view description, int priority) : PriorityBaseProvider<SoundResampler>(name, description, priority)
{
ProviderManager<SoundResampler>::Register(*this);
}

virtual ~SoundResampler()
{
ProviderManager<SoundResampler>::Unregister(*this);
}

virtual bool Resample(SoundEntry &sound, uint32_t play_rate) = 0;
};

#endif /* SOUNDLOADER_TYPE_H */
91 changes: 91 additions & 0 deletions src/soundresampler_soxr.cpp
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*/

/** @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 <soxr.h>
#include <thread>

#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<uint8_t> &in, std::vector<uint8_t> &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<uint8_t> &data = *sound.data;

std::vector<uint8_t> 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;
3 changes: 3 additions & 0 deletions vcpkg.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@
{
"name": "opusfile"
},
{
"name": "soxr"
},
{
"name": "sdl2",
"platform": "linux"
Expand Down

0 comments on commit 147d663

Please sign in to comment.