diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index ec71524a3..82e4850f7 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,4 +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 @@ -7,12 +11,12 @@ add_library(audio_core STATIC codec.cpp codec.h null_sink.h - stream.cpp - stream.h sink.h sink_details.cpp sink_details.h sink_stream.h + stream.cpp + stream.h $<$:cubeb_sink.cpp cubeb_sink.h> ) diff --git a/src/audio_core/algorithm/filter.cpp b/src/audio_core/algorithm/filter.cpp new file mode 100644 index 000000000..403b8503f --- /dev/null +++ b/src/audio_core/algorithm/filter.cpp @@ -0,0 +1,79 @@ +// 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 +#include "audio_core/algorithm/filter.h" +#include "common/common_types.h" + +namespace AudioCore { + +Filter Filter::LowPass(double cutoff, double Q) { + const double w0 = 2.0 * M_PI * cutoff; + const double sin_w0 = std::sin(w0); + const double cos_w0 = std::cos(w0); + const double alpha = sin_w0 / (2 * Q); + + const double a0 = 1 + alpha; + const double a1 = -2.0 * cos_w0; + const double a2 = 1 - alpha; + const double b0 = 0.5 * (1 - cos_w0); + const double b1 = 1.0 * (1 - cos_w0); + const double b2 = 0.5 * (1 - cos_w0); + + return {a0, a1, a2, b0, b1, b2}; +} + +Filter::Filter() : Filter(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) {} + +Filter::Filter(double a0, double a1, double a2, double b0, double b1, double b2) + : a1(a1 / a0), a2(a2 / a0), b0(b0 / a0), b1(b1 / a0), b2(b2 / a0) {} + +void Filter::Process(std::vector& signal) { + const size_t num_frames = signal.size() / 2; + for (size_t i = 0; i < num_frames; i++) { + std::rotate(in.begin(), in.end() - 1, in.end()); + std::rotate(out.begin(), out.end() - 1, out.end()); + + for (size_t ch = 0; ch < channel_count; ch++) { + in[0][ch] = signal[i * channel_count + ch]; + + out[0][ch] = b0 * in[0][ch] + b1 * in[1][ch] + b2 * in[2][ch] - a1 * out[1][ch] - + a2 * out[2][ch]; + + signal[i * 2 + ch] = std::clamp(out[0][ch], -32768.0, 32767.0); + } + } +} + +/// Calculates the appropriate Q for each biquad in a cascading filter. +/// @param total_count The total number of biquads to be cascaded. +/// @param index 0-index of the biquad to calculate the Q value for. +static double CascadingBiquadQ(size_t total_count, size_t index) { + const double pole = M_PI * (2 * index + 1) / (4.0 * total_count); + return 1.0 / (2.0 * std::cos(pole)); +} + +CascadingFilter CascadingFilter::LowPass(double cutoff, size_t cascade_size) { + std::vector cascade(cascade_size); + for (size_t i = 0; i < cascade_size; i++) { + cascade[i] = Filter::LowPass(cutoff, CascadingBiquadQ(cascade_size, i)); + } + return CascadingFilter{std::move(cascade)}; +} + +CascadingFilter::CascadingFilter() = default; +CascadingFilter::CascadingFilter(std::vector filters) : filters(std::move(filters)) {} + +void CascadingFilter::Process(std::vector& signal) { + for (auto& filter : filters) { + filter.Process(signal); + } +} + +} // namespace AudioCore diff --git a/src/audio_core/algorithm/filter.h b/src/audio_core/algorithm/filter.h new file mode 100644 index 000000000..a41beef98 --- /dev/null +++ b/src/audio_core/algorithm/filter.h @@ -0,0 +1,62 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include +#include "common/common_types.h" + +namespace AudioCore { + +/// Digital biquad filter: +/// +/// b0 + b1 z^-1 + b2 z^-2 +/// H(z) = ------------------------ +/// a0 + a1 z^-1 + b2 z^-2 +class Filter { +public: + /// Creates a low-pass filter. + /// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0. + /// @param Q Determines the quality factor of this filter. + static Filter LowPass(double cutoff, double Q = 0.7071); + + /// Passthrough filter. + Filter(); + + Filter(double a0, double a1, double a2, double b0, double b1, double b2); + + void Process(std::vector& signal); + +private: + static constexpr size_t channel_count = 2; + + /// Coefficients are in normalized form (a0 = 1.0). + double a1, a2, b0, b1, b2; + /// Input History + std::array, 3> in; + /// Output History + std::array, 3> out; +}; + +/// Cascade filters to build up higher-order filters from lower-order ones. +class CascadingFilter { +public: + /// Creates a cascading low-pass filter. + /// @param cutoff Determines the cutoff frequency. A value from 0.0 to 1.0. + /// @param cascade_size Number of biquads in cascade. + static CascadingFilter LowPass(double cutoff, size_t cascade_size); + + /// Passthrough. + CascadingFilter(); + + explicit CascadingFilter(std::vector filters); + + void Process(std::vector& signal); + +private: + std::vector filters; +}; + +} // namespace AudioCore diff --git a/src/audio_core/algorithm/interpolate.cpp b/src/audio_core/algorithm/interpolate.cpp new file mode 100644 index 000000000..11459821f --- /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 000000000..c79c2eef4 --- /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 6ebed3fb0..397b107f5 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; } @@ -224,7 +227,7 @@ void AudioRenderer::QueueMixedBuffer(Buffer::Tag tag) { break; } - samples_remaining -= samples.size(); + samples_remaining -= samples.size() / stream->GetNumChannels(); for (const auto& sample : samples) { const s32 buffer_sample{buffer[offset]}; diff --git a/src/audio_core/audio_renderer.h b/src/audio_core/audio_renderer.h index 13c5d0adc..eba67f28e 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{};