diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index 92322f59bc..82e4850f74 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,6 +1,8 @@ add_library(audio_core STATIC algorithm/filter.cpp algorithm/filter.h + algorithm/interpolate.cpp + algorithm/interpolate.h audio_out.cpp audio_out.h audio_renderer.cpp diff --git a/src/audio_core/algorithm/interpolate.cpp b/src/audio_core/algorithm/interpolate.cpp new file mode 100644 index 0000000000..11459821f9 --- /dev/null +++ b/src/audio_core/algorithm/interpolate.cpp @@ -0,0 +1,71 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#define _USE_MATH_DEFINES + +#include +#include +#include +#include "audio_core/algorithm/interpolate.h" +#include "common/common_types.h" +#include "common/logging/log.h" + +namespace AudioCore { + +/// The Lanczos kernel +static double Lanczos(size_t a, double x) { + if (x == 0.0) + return 1.0; + const double px = M_PI * x; + return a * std::sin(px) * std::sin(px / a) / (px * px); +} + +std::vector Interpolate(InterpolationState& state, std::vector input, double ratio) { + if (input.size() < 2) + return {}; + + if (ratio <= 0) { + LOG_CRITICAL(Audio, "Nonsensical interpolation ratio {}", ratio); + ratio = 1.0; + } + + if (ratio != state.current_ratio) { + const double cutoff_frequency = std::min(0.5 / ratio, 0.5 * ratio); + state.nyquist = CascadingFilter::LowPass(std::clamp(cutoff_frequency, 0.0, 0.4), 3); + state.current_ratio = ratio; + } + state.nyquist.Process(input); + + constexpr size_t taps = InterpolationState::lanczos_taps; + const size_t num_frames = input.size() / 2; + + std::vector output; + output.reserve(static_cast(input.size() / ratio + 4)); + + double& pos = state.position; + auto& h = state.history; + for (size_t i = 0; i < num_frames; ++i) { + std::rotate(h.begin(), h.end() - 1, h.end()); + h[0][0] = input[i * 2 + 0]; + h[0][1] = input[i * 2 + 1]; + + while (pos <= 1.0) { + double l = 0.0; + double r = 0.0; + for (size_t j = 0; j < h.size(); j++) { + l += Lanczos(taps, pos + j - taps + 1) * h[j][0]; + r += Lanczos(taps, pos + j - taps + 1) * h[j][1]; + } + output.emplace_back(static_cast(std::clamp(l, -32768.0, 32767.0))); + output.emplace_back(static_cast(std::clamp(r, -32768.0, 32767.0))); + + pos += ratio; + } + pos -= 1.0; + } + + return output; +} + +} // namespace AudioCore diff --git a/src/audio_core/algorithm/interpolate.h b/src/audio_core/algorithm/interpolate.h new file mode 100644 index 0000000000..c79c2eef41 --- /dev/null +++ b/src/audio_core/algorithm/interpolate.h @@ -0,0 +1,43 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "audio_core/algorithm/filter.h" +#include "common/common_types.h" + +namespace AudioCore { + +struct InterpolationState { + static constexpr size_t lanczos_taps = 4; + static constexpr size_t history_size = lanczos_taps * 2 - 1; + + double current_ratio = 0.0; + CascadingFilter nyquist; + std::array, history_size> history = {}; + double position = 0; +}; + +/// Interpolates input signal to produce output signal. +/// @param input The signal to interpolate. +/// @param ratio Interpolation ratio. +/// ratio > 1.0 results in fewer output samples. +/// ratio < 1.0 results in more output samples. +/// @returns Output signal. +std::vector Interpolate(InterpolationState& state, std::vector input, double ratio); + +/// Interpolates input signal to produce output signal. +/// @param input The signal to interpolate. +/// @param input_rate The sample rate of input. +/// @param output_rate The desired sample rate of the output. +/// @returns Output signal. +inline std::vector Interpolate(InterpolationState& state, std::vector input, + u32 input_rate, u32 output_rate) { + const double ratio = static_cast(input_rate) / static_cast(output_rate); + return Interpolate(state, std::move(input), ratio); +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_renderer.cpp b/src/audio_core/audio_renderer.cpp index 6ebed3fb0d..7bff635b8c 100644 --- a/src/audio_core/audio_renderer.cpp +++ b/src/audio_core/audio_renderer.cpp @@ -2,6 +2,7 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include "audio_core/algorithm/interpolate.h" #include "audio_core/audio_renderer.h" #include "common/assert.h" #include "common/logging/log.h" @@ -199,6 +200,8 @@ void AudioRenderer::VoiceState::RefreshBuffer() { break; } + samples = Interpolate(interp_state, std::move(samples), Info().sample_rate, STREAM_SAMPLE_RATE); + is_refresh_pending = false; } diff --git a/src/audio_core/audio_renderer.h b/src/audio_core/audio_renderer.h index 13c5d0adc9..eba67f28e1 100644 --- a/src/audio_core/audio_renderer.h +++ b/src/audio_core/audio_renderer.h @@ -8,6 +8,7 @@ #include #include +#include "audio_core/algorithm/interpolate.h" #include "audio_core/audio_out.h" #include "audio_core/codec.h" #include "audio_core/stream.h" @@ -194,6 +195,7 @@ private: size_t wave_index{}; size_t offset{}; Codec::ADPCMState adpcm_state{}; + InterpolationState interp_state{}; std::vector samples; VoiceOutStatus out_status{}; VoiceInfo info{};