diff --git a/CMakeLists.txt b/CMakeLists.txt index 74fafc04..3193701d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -294,6 +294,7 @@ set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selecto ) set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp src/core/audio/teakra_core.cpp src/core/audio/miniaudio_device.cpp src/core/audio/hle_core.cpp src/core/audio/aac_decoder.cpp + src/core/audio/audio_interpolation.cpp ) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) @@ -334,6 +335,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp include/PICA/pica_vert_config.hpp include/sdl_sensors.hpp include/PICA/draw_acceleration.hpp include/renderdoc.hpp include/align.hpp include/audio/aac_decoder.hpp include/PICA/pica_simd.hpp include/services/fonts.hpp + include/audio/audio_interpolation.hpp include/audio/hle_mixer.hpp ) cmrc_add_resource_library( diff --git a/include/audio/audio_interpolation.hpp b/include/audio/audio_interpolation.hpp new file mode 100644 index 00000000..8a87cbcd --- /dev/null +++ b/include/audio/audio_interpolation.hpp @@ -0,0 +1,58 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include +#include + +#include "audio/hle_mixer.hpp" +#include "helpers.hpp" + +namespace Audio::Interpolation { + // A variable length buffer of signed PCM16 stereo samples. + using StereoBuffer16 = std::deque>; + using StereoFrame16 = Audio::DSPMixer::StereoFrame; + + struct State { + // Two historical samples. + std::array xn1 = {}; //< x[n-1] + std::array xn2 = {}; //< x[n-2] + // Current fractional position. + u64 fposition = 0; + }; + + /** + * No interpolation. This is equivalent to a zero-order hold. There is a two-sample predelay. + * @param state Interpolation state. + * @param input Input buffer. + * @param rate Stretch factor. Must be a positive non-zero value. + * rate > 1.0 performs decimation and rate < 1.0 performs upsampling. + * @param output The resampled audio buffer. + * @param outputi The index of output to start writing to. + */ + void none(State& state, StereoBuffer16& input, float rate, StereoFrame16& output, usize& outputi); + + /** + * Linear interpolation. This is equivalent to a first-order hold. There is a two-sample predelay. + * @param state Interpolation state. + * @param input Input buffer. + * @param rate Stretch factor. Must be a positive non-zero value. + * rate > 1.0 performs decimation and rate < 1.0 performs upsampling. + * @param output The resampled audio buffer. + * @param outputi The index of output to start writing to. + */ + void linear(State& state, StereoBuffer16& input, float rate, StereoFrame16& output, usize& outputi); + + /** + * Polyphase interpolation. This is currently stubbed to just perform linear interpolation + * @param state Interpolation state. + * @param input Input buffer. + * @param rate Stretch factor. Must be a positive non-zero value. + * rate > 1.0 performs decimation and rate < 1.0 performs upsampling. + * @param output The resampled audio buffer. + * @param outputi The index of output to start writing to. + */ + void polyphase(State& state, StereoBuffer16& input, float rate, StereoFrame16& output, usize& outputi); +} // namespace Audio::Interpolation \ No newline at end of file diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 29fd4542..b3832d76 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -8,54 +8,13 @@ #include "audio/aac.hpp" #include "audio/aac_decoder.hpp" +#include "audio/audio_interpolation.hpp" #include "audio/dsp_core.hpp" #include "audio/dsp_shared_mem.hpp" +#include "audio/hle_mixer.hpp" #include "memory.hpp" namespace Audio { - using SampleFormat = HLE::SourceConfiguration::Configuration::Format; - using SourceType = HLE::SourceConfiguration::Configuration::MonoOrStereo; - - class DSPMixer { - public: - template - using Sample = std::array; - - template - using Frame = std::array, 160>; - - template - using MonoFrame = Frame; - - template - using StereoFrame = Frame; - - template - using QuadFrame = Frame; - - // Internally the DSP uses four channels when mixing. - // Neatly, QuadFrame means that every sample is a uint32x4 value, which is particularly nice for SIMD mixing - using IntermediateMix = QuadFrame; - - private: - using ChannelFormat = HLE::DspConfiguration::OutputFormat; - // The audio from each DSP voice is converted to quadraphonic and then fed into 3 intermediate mixing stages - // Two of these intermediate mixers (second and third) are used for effects, including custom effects done on the CPU - static constexpr usize mixerStageCount = 3; - - public: - ChannelFormat channelFormat = ChannelFormat::Stereo; - std::array volumes; - std::array enableAuxStages; - - void reset() { - channelFormat = ChannelFormat::Stereo; - - volumes.fill(0.0); - enableAuxStages.fill(false); - } - }; - struct DSPSource { // Audio buffer information // https://www.3dbrew.org/wiki/DSP_Memory_Region @@ -89,6 +48,7 @@ namespace Audio { using SampleBuffer = std::deque>; using BufferQueue = std::priority_queue; using InterpolationMode = HLE::SourceConfiguration::Configuration::InterpolationMode; + using InterpolationState = Audio::Interpolation::State; DSPMixer::StereoFrame currentFrame; BufferQueue buffers; @@ -96,6 +56,7 @@ namespace Audio { SampleFormat sampleFormat = SampleFormat::ADPCM; SourceType sourceType = SourceType::Stereo; InterpolationMode interpolationMode = InterpolationMode::Linear; + InterpolationState interpolationState; // There's one gain configuration for each of the 3 intermediate mixing stages // And each gain configuration is composed of 4 gain values, one for each sample in a quad-channel sample diff --git a/include/audio/hle_mixer.hpp b/include/audio/hle_mixer.hpp new file mode 100644 index 00000000..ed8b4a09 --- /dev/null +++ b/include/audio/hle_mixer.hpp @@ -0,0 +1,50 @@ +#pragma once +#include + +#include "audio/dsp_shared_mem.hpp" +#include "helpers.hpp" + +namespace Audio { + using SampleFormat = HLE::SourceConfiguration::Configuration::Format; + using SourceType = HLE::SourceConfiguration::Configuration::MonoOrStereo; + + class DSPMixer { + public: + template + using Sample = std::array; + + template + using Frame = std::array, 160>; + + template + using MonoFrame = Frame; + + template + using StereoFrame = Frame; + + template + using QuadFrame = Frame; + + // Internally the DSP uses four channels when mixing. + // Neatly, QuadFrame means that every sample is a uint32x4 value, which is particularly nice for SIMD mixing + using IntermediateMix = QuadFrame; + + private: + using ChannelFormat = HLE::DspConfiguration::OutputFormat; + // The audio from each DSP voice is converted to quadraphonic and then fed into 3 intermediate mixing stages + // Two of these intermediate mixers (second and third) are used for effects, including custom effects done on the CPU + static constexpr usize mixerStageCount = 3; + + public: + ChannelFormat channelFormat = ChannelFormat::Stereo; + std::array volumes; + std::array enableAuxStages; + + void reset() { + channelFormat = ChannelFormat::Stereo; + + volumes.fill(0.0); + enableAuxStages.fill(false); + } + }; +} // namespace Audio \ No newline at end of file diff --git a/src/core/audio/audio_interpolation.cpp b/src/core/audio/audio_interpolation.cpp new file mode 100644 index 00000000..d13c786e --- /dev/null +++ b/src/core/audio/audio_interpolation.cpp @@ -0,0 +1,73 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio/audio_interpolation.hpp" + +#include + +#include "helpers.hpp" + +namespace Audio::Interpolation { + // Calculations are done in fixed point with 24 fractional bits. + // (This is not verified. This was chosen for minimal error.) + static constexpr u64 scaleFactor = 1 << 24; + static constexpr u64 scaleMask = scaleFactor - 1; + + /// Here we step over the input in steps of rate, until we consume all of the input. + /// Three adjacent samples are passed to fn each step. + template + static void stepOverSamples(State& state, StereoBuffer16& input, float rate, StereoFrame16& output, usize& outputi, Function fn) { + if (input.empty()) { + return; + } + + input.insert(input.begin(), {state.xn2, state.xn1}); + + const u64 step_size = static_cast(rate * scaleFactor); + u64 fposition = state.fposition; + usize inputi = 0; + + while (outputi < output.size()) { + inputi = static_cast(fposition / scaleFactor); + + if (inputi + 2 >= input.size()) { + inputi = input.size() - 2; + break; + } + + u64 fraction = fposition & scaleMask; + output[outputi++] = fn(fraction, input[inputi], input[inputi + 1], input[inputi + 2]); + + fposition += step_size; + } + + state.xn2 = input[inputi]; + state.xn1 = input[inputi + 1]; + state.fposition = fposition - inputi * scaleFactor; + + input.erase(input.begin(), std::next(input.begin(), inputi + 2)); + } + + void none(State& state, StereoBuffer16& input, float rate, StereoFrame16& output, usize& outputi) { + stepOverSamples(state, input, rate, output, outputi, [](u64 fraction, const auto& x0, const auto& x1, const auto& x2) { return x0; }); + } + + void linear(State& state, StereoBuffer16& input, float rate, StereoFrame16& output, usize& outputi) { + // Note on accuracy: Some values that this produces are +/- 1 from the actual firmware. + stepOverSamples(state, input, rate, output, outputi, [](u64 fraction, const auto& x0, const auto& x1, const auto& x2) { + // This is a saturated subtraction. (Verified by black-box fuzzing.) + s64 delta0 = std::clamp(x1[0] - x0[0], -32768, 32767); + s64 delta1 = std::clamp(x1[1] - x0[1], -32768, 32767); + + return std::array{ + static_cast(x0[0] + fraction * delta0 / scaleFactor), + static_cast(x0[1] + fraction * delta1 / scaleFactor), + }; + }); + } + + void polyphase(State& state, StereoBuffer16& input, float rate, StereoFrame16& output, usize& outputi) { + linear(state, input, rate, output, outputi); + } +} // namespace Audio::Interpolation \ No newline at end of file diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 06e001f1..593dd477 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -732,6 +732,7 @@ namespace Audio { rateMultiplier = 1.f; buffers = {}; + interpolationState = {}; currentSamples.clear(); gains.fill({});