Skip to content

Commit ab2e1ee

Browse files
committed
Add: Use LibSoxr for high quality sample-rate conversion.
If not present the existing bad quality linear conversion can still be used.
1 parent 5dea170 commit ab2e1ee

File tree

12 files changed

+186
-0
lines changed

12 files changed

+186
-0
lines changed

.github/workflows/ci-linux.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ jobs:
5656
libogg-dev \
5757
libopus-dev \
5858
libopusfile-dev \
59+
libsoxr-dev \
5960
${{ inputs.libraries }} \
6061
zlib1g-dev \
6162
# EOF

.github/workflows/ci-mingw.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ jobs:
4040
mingw-w64-${{ inputs.arch }}-libogg
4141
mingw-w64-${{ inputs.arch }}-opus
4242
mingw-w64-${{ inputs.arch }}-opusfile
43+
mingw-w64-${{ inputs.arch }}-libsoxr
4344
4445
- name: Install OpenGFX
4546
shell: bash

.github/workflows/codeql.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ jobs:
5050
libopus-dev \
5151
libopusfile-dev \
5252
libsdl2-dev \
53+
libsoxr-dev \
5354
zlib1g-dev \
5455
# EOF
5556

CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ if(NOT OPTION_DEDICATED)
152152
endif()
153153
endif()
154154
find_package(OpusFile)
155+
find_package(Soxr)
155156
endif()
156157
if(APPLE)
157158
enable_language(OBJCXX)
@@ -326,6 +327,7 @@ if(NOT OPTION_DEDICATED)
326327
link_package(ICU_i18n)
327328
link_package(ICU_uc)
328329
link_package(OpusFile TARGET OpusFile::opusfile)
330+
link_package(Soxr)
329331

330332
if(SDL2_FOUND AND OPENGL_FOUND AND UNIX)
331333
# SDL2 dynamically loads OpenGL if needed, so do not link to OpenGL when

cmake/FindSoxr.cmake

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
include(FindPackageHandleStandardArgs)
2+
3+
find_library(Soxr_LIBRARY
4+
NAMES soxr
5+
)
6+
7+
set(Soxr_COMPILE_OPTIONS "" CACHE STRING "Extra compile options of soxr")
8+
9+
set(Soxr_LINK_LIBRARIES "" CACHE STRING "Extra link libraries of soxr")
10+
11+
set(Soxr_LINK_FLAGS "" CACHE STRING "Extra link flags of soxr")
12+
13+
find_path(Soxr_INCLUDE_PATH
14+
NAMES soxr.h
15+
)
16+
17+
find_package_handle_standard_args(Soxr
18+
REQUIRED_VARS Soxr_LIBRARY Soxr_INCLUDE_PATH
19+
)
20+
21+
if(Soxr_FOUND)
22+
if(NOT TARGET Soxr::soxr)
23+
add_library(Soxr::soxr UNKNOWN IMPORTED)
24+
set_target_properties(Soxr::soxr PROPERTIES
25+
IMPORTED_LOCATION "${Soxr_LIBRARY}"
26+
INTERFACE_INCLUDE_DIRECTORIES "${Soxr_INCLUDE_PATH}"
27+
INTERFACE_COMPILE_OPTIONS "${Soxr_COMPILE_OPTIONS}"
28+
INTERFACE_LINK_LIBRARIES "${Soxr_LINK_LIBRARIES}"
29+
INTERFACE_LINK_FLAGS "${Soxr_LINK_FLAGS}"
30+
)
31+
endif()
32+
endif()

src/CMakeLists.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ add_files(
5050
CONDITION OpusFile_FOUND
5151
)
5252

53+
add_files(
54+
soundresampler_soxr.cpp
55+
CONDITION Soxr_FOUND
56+
)
57+
5358
add_files(
5459
aircraft.h
5560
aircraft_cmd.cpp
@@ -448,6 +453,7 @@ add_files(
448453
soundloader_type.h
449454
soundloader_raw.cpp
450455
soundloader_wav.cpp
456+
soundresampler_type.h
451457
source_type.h
452458
sprite.cpp
453459
sprite.h

src/mixer.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ bool MxInitialize(uint rate)
248248
return true;
249249
}
250250

251+
uint32_t MxGetRate()
252+
{
253+
return _play_rate;
254+
}
255+
251256
void SetEffectVolume(uint8_t volume)
252257
{
253258
_effect_vol.store(volume, std::memory_order_relaxed);

src/mixer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ struct MixerChannel;
2121
typedef void(*MxStreamCallback)(int16_t *buffer, size_t samples);
2222

2323
bool MxInitialize(uint rate);
24+
uint32_t MxGetRate();
2425
void MxMixSamples(void *buffer, uint samples);
2526

2627
MixerChannel *MxAllocateChannel();

src/soundloader.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@
1212
#include "sound_type.h"
1313
#include "soundloader_type.h"
1414
#include "soundloader_func.h"
15+
#include "soundresampler_type.h"
1516
#include "string_func.h"
17+
#include "mixer.h"
1618
#include "newgrf_sound.h"
1719
#include "random_access_file_type.h"
1820

1921
#include "safeguards.h"
2022

2123
template class ProviderManager<SoundLoader>;
24+
template class ProviderManager<SoundResampler>;
2225

2326
bool LoadSoundData(SoundEntry &sound, bool new_format, SoundID sound_id, const std::string &name)
2427
{
@@ -43,6 +46,14 @@ bool LoadSoundData(SoundEntry &sound, bool new_format, SoundID sound_id, const s
4346

4447
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);
4548

49+
/* Convert sample rate if needed. */
50+
const uint32_t play_rate = MxGetRate();
51+
if (play_rate != sound.rate) {
52+
for (auto &resampler : ProviderManager<SoundResampler>::GetProviders()) {
53+
if (resampler->Resample(sound, play_rate)) break;
54+
}
55+
}
56+
4657
/* Mixer always requires an extra sample at the end for the built-in linear resampler. */
4758
sound.data->resize(sound.data->size() + sound.channels * sound.bits_per_sample / 8);
4859
sound.data->shrink_to_fit();

src/soundresampler_soxr.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* This file is part of OpenTTD.
3+
* 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.
4+
* 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.
5+
* 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/>.
6+
*/
7+
8+
/** @file soundresampler_soxr.cpp SOXR sound resampler. */
9+
10+
#include "stdafx.h"
11+
#include "core/math_func.hpp"
12+
#include "debug.h"
13+
#include "sound_type.h"
14+
#include "soundresampler_type.h"
15+
16+
#include <soxr.h>
17+
#include <thread>
18+
19+
#include "safeguards.h"
20+
21+
class SoundResampler_Soxr : public SoundResampler {
22+
public:
23+
SoundResampler_Soxr() : SoundResampler("soxr", "SOXR sound resampler", 0) {}
24+
25+
/**
26+
* Convert samples from 8 bits to 16 bits.
27+
* @param in Vector of samples to convert.
28+
* @param out Vector to place converted samples.
29+
* @pre out vector must be exactly twice the size of in vector.
30+
*/
31+
static void ConvertInt8toInt16(std::vector<std::byte> &in, std::vector<std::byte> &out)
32+
{
33+
assert(std::size(out) == std::size(in) * 2);
34+
35+
auto out_it = std::begin(out);
36+
for (const std::byte &value : in) {
37+
if constexpr (std::endian::native != std::endian::little) {
38+
*out_it++ = value;
39+
*out_it++ = std::byte{0};
40+
} else {
41+
*out_it++ = std::byte{0};
42+
*out_it++ = value;
43+
}
44+
}
45+
}
46+
47+
bool Resample(SoundEntry &sound, uint32_t play_rate) override
48+
{
49+
/* The sound data to work on. */
50+
std::vector<std::byte> &data = *sound.data;
51+
52+
std::vector<std::byte> tmp;
53+
if (sound.bits_per_sample == 16) {
54+
/* No conversion necessary so just move from sound data to temporary buffer. */
55+
data.swap(tmp);
56+
} else {
57+
/* SoxR cannot resample 8-bit audio, so convert from 8-bit to 16-bit into temporary buffer. */
58+
tmp.resize(std::size(data) * sizeof(int16_t));
59+
ConvertInt8toInt16(data, tmp);
60+
sound.bits_per_sample = 16;
61+
}
62+
63+
/* Resize buffer ensuring it is correctly aligned. */
64+
uint align = sound.channels * sound.bits_per_sample / 8;
65+
data.resize(Align(std::size(tmp) * play_rate / sound.rate, align));
66+
67+
soxr_error_t error = soxr_oneshot(sound.rate, play_rate, sound.channels,
68+
std::data(tmp), std::size(tmp) / align, nullptr,
69+
std::data(data), std::size(data) / align, nullptr,
70+
&this->io, &this->quality, &this->runtime);
71+
72+
if (error != nullptr) {
73+
/* Could not resample, try using the original data as-is without resampling instead. */
74+
Debug(misc, 0, "Failed to resample: {}", soxr_strerror(error));
75+
data.swap(tmp);
76+
} else {
77+
sound.rate = play_rate;
78+
}
79+
80+
return true;
81+
}
82+
83+
private:
84+
static SoundResampler_Soxr instance;
85+
86+
const soxr_io_spec_t io = soxr_io_spec(SOXR_INT16_I, SOXR_INT16_I); // 16-bit input and output.
87+
const soxr_quality_spec_t quality = soxr_quality_spec(SOXR_VHQ, 0); // Use 'Very high quality'.
88+
const soxr_runtime_spec_t runtime = soxr_runtime_spec(std::thread::hardware_concurrency()); // Enable multi-threading.
89+
};
90+
91+
/* static */ SoundResampler_Soxr SoundResampler_Soxr::instance{};

0 commit comments

Comments
 (0)