diff options
Diffstat (limited to 'src/audio_core')
| -rw-r--r-- | src/audio_core/CMakeLists.txt | 17 | ||||
| -rw-r--r-- | src/audio_core/audio_core.cpp | 46 | ||||
| -rw-r--r-- | src/audio_core/audio_core.h | 7 | ||||
| -rw-r--r-- | src/audio_core/codec.cpp | 122 | ||||
| -rw-r--r-- | src/audio_core/codec.h | 50 | ||||
| -rw-r--r-- | src/audio_core/hle/common.h | 36 | ||||
| -rw-r--r-- | src/audio_core/hle/dsp.cpp | 69 | ||||
| -rw-r--r-- | src/audio_core/hle/dsp.h | 55 | ||||
| -rw-r--r-- | src/audio_core/hle/filter.cpp | 115 | ||||
| -rw-r--r-- | src/audio_core/hle/filter.h | 113 | ||||
| -rw-r--r-- | src/audio_core/hle/pipe.cpp | 32 | ||||
| -rw-r--r-- | src/audio_core/hle/pipe.h | 4 | ||||
| -rw-r--r-- | src/audio_core/hle/source.cpp | 320 | ||||
| -rw-r--r-- | src/audio_core/hle/source.h | 144 | ||||
| -rw-r--r-- | src/audio_core/interpolate.cpp | 85 | ||||
| -rw-r--r-- | src/audio_core/interpolate.h | 41 | ||||
| -rw-r--r-- | src/audio_core/null_sink.h | 29 | ||||
| -rw-r--r-- | src/audio_core/sink_details.cpp | 18 | ||||
| -rw-r--r-- | src/audio_core/sink_details.h | 27 |
19 files changed, 1268 insertions, 62 deletions
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt index b0d1c7eb6..4cd7aba67 100644 --- a/src/audio_core/CMakeLists.txt +++ b/src/audio_core/CMakeLists.txt @@ -1,16 +1,31 @@ set(SRCS audio_core.cpp + codec.cpp hle/dsp.cpp + hle/filter.cpp hle/pipe.cpp + hle/source.cpp + interpolate.cpp + sink_details.cpp ) set(HEADERS audio_core.h + codec.h + hle/common.h hle/dsp.h + hle/filter.h hle/pipe.h + hle/source.h + interpolate.h + null_sink.h sink.h + sink_details.h ) +include_directories(../../externals/soundtouch/include) + create_directory_groups(${SRCS} ${HEADERS}) -add_library(audio_core STATIC ${SRCS} ${HEADERS})
\ No newline at end of file +add_library(audio_core STATIC ${SRCS} ${HEADERS}) +target_link_libraries(audio_core SoundTouch) diff --git a/src/audio_core/audio_core.cpp b/src/audio_core/audio_core.cpp index 894f46990..d42249ebd 100644 --- a/src/audio_core/audio_core.cpp +++ b/src/audio_core/audio_core.cpp @@ -2,8 +2,15 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <memory> +#include <string> + #include "audio_core/audio_core.h" #include "audio_core/hle/dsp.h" +#include "audio_core/hle/pipe.h" +#include "audio_core/null_sink.h" +#include "audio_core/sink.h" +#include "audio_core/sink_details.h" #include "core/core_timing.h" #include "core/hle/kernel/vm_manager.h" @@ -17,17 +24,16 @@ static constexpr u64 audio_frame_ticks = 1310252ull; ///< Units: ARM11 cycles static void AudioTickCallback(u64 /*userdata*/, int cycles_late) { if (DSP::HLE::Tick()) { - // HACK: We're not signaling the interrups when they should be, but just firing them all off together. - // It should be only (interrupt_id = 2, channel_id = 2) that's signalled here. - // TODO(merry): Understand when the other interrupts are fired. - DSP_DSP::SignalAllInterrupts(); + // TODO(merry): Signal all the other interrupts as appropriate. + DSP_DSP::SignalPipeInterrupt(DSP::HLE::DspPipe::Audio); + // HACK(merry): Added to prevent regressions. Will remove soon. + DSP_DSP::SignalPipeInterrupt(DSP::HLE::DspPipe::Binary); } // Reschedule recurrent event CoreTiming::ScheduleEvent(audio_frame_ticks - cycles_late, tick_event); } -/// Initialise Audio void Init() { DSP::HLE::Init(); @@ -35,19 +41,39 @@ void Init() { CoreTiming::ScheduleEvent(audio_frame_ticks, tick_event); } -/// Add DSP address spaces to Process's address space. void AddAddressSpace(Kernel::VMManager& address_space) { - auto r0_vma = address_space.MapBackingMemory(DSP::HLE::region0_base, reinterpret_cast<u8*>(&DSP::HLE::g_region0), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom(); + auto r0_vma = address_space.MapBackingMemory(DSP::HLE::region0_base, reinterpret_cast<u8*>(&DSP::HLE::g_regions[0]), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom(); address_space.Reprotect(r0_vma, Kernel::VMAPermission::ReadWrite); - auto r1_vma = address_space.MapBackingMemory(DSP::HLE::region1_base, reinterpret_cast<u8*>(&DSP::HLE::g_region1), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom(); + auto r1_vma = address_space.MapBackingMemory(DSP::HLE::region1_base, reinterpret_cast<u8*>(&DSP::HLE::g_regions[1]), sizeof(DSP::HLE::SharedMemory), Kernel::MemoryState::IO).MoveFrom(); address_space.Reprotect(r1_vma, Kernel::VMAPermission::ReadWrite); } -/// Shutdown Audio +void SelectSink(std::string sink_id) { + if (sink_id == "auto") { + // Auto-select. + // g_sink_details is ordered in terms of desirability, with the best choice at the front. + const auto& sink_detail = g_sink_details.front(); + DSP::HLE::SetSink(sink_detail.factory()); + return; + } + + auto iter = std::find_if(g_sink_details.begin(), g_sink_details.end(), [sink_id](const auto& sink_detail) { + return sink_detail.id == sink_id; + }); + + if (iter == g_sink_details.end()) { + LOG_ERROR(Audio, "AudioCore::SelectSink given invalid sink_id"); + DSP::HLE::SetSink(std::make_unique<NullSink>()); + return; + } + + DSP::HLE::SetSink(iter->factory()); +} + void Shutdown() { CoreTiming::UnscheduleEvent(tick_event, 0); DSP::HLE::Shutdown(); } -} //namespace +} // namespace AudioCore diff --git a/src/audio_core/audio_core.h b/src/audio_core/audio_core.h index 64c330914..f618361f3 100644 --- a/src/audio_core/audio_core.h +++ b/src/audio_core/audio_core.h @@ -4,14 +4,14 @@ #pragma once +#include <string> + namespace Kernel { class VMManager; } namespace AudioCore { -constexpr int num_sources = 24; -constexpr int samples_per_frame = 160; ///< Samples per audio frame at native sample rate constexpr int native_sample_rate = 32728; ///< 32kHz /// Initialise Audio Core @@ -20,6 +20,9 @@ void Init(); /// Add DSP address spaces to a Process. void AddAddressSpace(Kernel::VMManager& vm_manager); +/// Select the sink to use based on sink id. +void SelectSink(std::string sink_id); + /// Shutdown Audio Core void Shutdown(); diff --git a/src/audio_core/codec.cpp b/src/audio_core/codec.cpp new file mode 100644 index 000000000..ab65514b7 --- /dev/null +++ b/src/audio_core/codec.cpp @@ -0,0 +1,122 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <array> +#include <cstddef> +#include <cstring> +#include <vector> + +#include "audio_core/codec.h" + +#include "common/assert.h" +#include "common/common_types.h" +#include "common/math_util.h" + +namespace Codec { + +StereoBuffer16 DecodeADPCM(const u8* const data, const size_t sample_count, const std::array<s16, 16>& adpcm_coeff, ADPCMState& state) { + // GC-ADPCM with scale factor and variable coefficients. + // Frames are 8 bytes long containing 14 samples each. + // Samples are 4 bits (one nibble) long. + + constexpr size_t FRAME_LEN = 8; + constexpr size_t SAMPLES_PER_FRAME = 14; + constexpr std::array<int, 16> SIGNED_NIBBLES {{ 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1 }}; + + const size_t ret_size = sample_count % 2 == 0 ? sample_count : sample_count + 1; // Ensure multiple of two. + StereoBuffer16 ret(ret_size); + + int yn1 = state.yn1, + yn2 = state.yn2; + + const size_t NUM_FRAMES = (sample_count + (SAMPLES_PER_FRAME - 1)) / SAMPLES_PER_FRAME; // Round up. + for (size_t framei = 0; framei < NUM_FRAMES; framei++) { + const int frame_header = data[framei * FRAME_LEN]; + const int scale = 1 << (frame_header & 0xF); + const int idx = (frame_header >> 4) & 0x7; + + // Coefficients are fixed point with 11 bits fractional part. + const int coef1 = adpcm_coeff[idx * 2 + 0]; + const int coef2 = adpcm_coeff[idx * 2 + 1]; + + // Decodes an audio sample. One nibble produces one sample. + const auto decode_sample = [&](const int nibble) -> s16 { + const int xn = nibble * scale; + // We first transform everything into 11 bit fixed point, perform the second order digital filter, then transform back. + // 0x400 == 0.5 in 11 bit fixed point. + // Filter: y[n] = x[n] + 0.5 + c1 * y[n-1] + c2 * y[n-2] + int val = ((xn << 11) + 0x400 + coef1 * yn1 + coef2 * yn2) >> 11; + // Clamp to output range. + val = MathUtil::Clamp(val, -32768, 32767); + // Advance output feedback. + yn2 = yn1; + yn1 = val; + return (s16)val; + }; + + size_t outputi = framei * SAMPLES_PER_FRAME; + size_t datai = framei * FRAME_LEN + 1; + for (size_t i = 0; i < SAMPLES_PER_FRAME && outputi < sample_count; i += 2) { + const s16 sample1 = decode_sample(SIGNED_NIBBLES[data[datai] & 0xF]); + ret[outputi].fill(sample1); + outputi++; + + const s16 sample2 = decode_sample(SIGNED_NIBBLES[data[datai] >> 4]); + ret[outputi].fill(sample2); + outputi++; + + datai++; + } + } + + state.yn1 = yn1; + state.yn2 = yn2; + + return ret; +} + +static s16 SignExtendS8(u8 x) { + // The data is actually signed PCM8. + // We sign extend this to signed PCM16. + return static_cast<s16>(static_cast<s8>(x)); +} + +StereoBuffer16 DecodePCM8(const unsigned num_channels, const u8* const data, const size_t sample_count) { + ASSERT(num_channels == 1 || num_channels == 2); + + StereoBuffer16 ret(sample_count); + + if (num_channels == 1) { + for (size_t i = 0; i < sample_count; i++) { + ret[i].fill(SignExtendS8(data[i])); + } + } else { + for (size_t i = 0; i < sample_count; i++) { + ret[i][0] = SignExtendS8(data[i * 2 + 0]); + ret[i][1] = SignExtendS8(data[i * 2 + 1]); + } + } + + return ret; +} + +StereoBuffer16 DecodePCM16(const unsigned num_channels, const u8* const data, const size_t sample_count) { + ASSERT(num_channels == 1 || num_channels == 2); + + StereoBuffer16 ret(sample_count); + + if (num_channels == 1) { + for (size_t i = 0; i < sample_count; i++) { + s16 sample; + std::memcpy(&sample, data + i * sizeof(s16), sizeof(s16)); + ret[i].fill(sample); + } + } else { + std::memcpy(ret.data(), data, sample_count * 2 * sizeof(u16)); + } + + return ret; +} + +}; diff --git a/src/audio_core/codec.h b/src/audio_core/codec.h new file mode 100644 index 000000000..e695f2edc --- /dev/null +++ b/src/audio_core/codec.h @@ -0,0 +1,50 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <vector> + +#include "common/common_types.h" + +namespace Codec { + +/// A variable length buffer of signed PCM16 stereo samples. +using StereoBuffer16 = std::vector<std::array<s16, 2>>; + +/// See: Codec::DecodeADPCM +struct ADPCMState { + // Two historical samples from previous processed buffer, + // required for ADPCM decoding + s16 yn1; ///< y[n-1] + s16 yn2; ///< y[n-2] +}; + +/** + * @param data Pointer to buffer that contains ADPCM data to decode + * @param sample_count Length of buffer in terms of number of samples + * @param adpcm_coeff ADPCM coefficients + * @param state ADPCM state, this is updated with new state + * @return Decoded stereo signed PCM16 data, sample_count in length + */ +StereoBuffer16 DecodeADPCM(const u8* const data, const size_t sample_count, const std::array<s16, 16>& adpcm_coeff, ADPCMState& state); + +/** + * @param num_channels Number of channels + * @param data Pointer to buffer that contains PCM8 data to decode + * @param sample_count Length of buffer in terms of number of samples + * @return Decoded stereo signed PCM16 data, sample_count in length + */ +StereoBuffer16 DecodePCM8(const unsigned num_channels, const u8* const data, const size_t sample_count); + +/** + * @param num_channels Number of channels + * @param data Pointer to buffer that contains PCM16 data to decode + * @param sample_count Length of buffer in terms of number of samples + * @return Decoded stereo signed PCM16 data, sample_count in length + */ +StereoBuffer16 DecodePCM16(const unsigned num_channels, const u8* const data, const size_t sample_count); + +}; diff --git a/src/audio_core/hle/common.h b/src/audio_core/hle/common.h new file mode 100644 index 000000000..596b67eaf --- /dev/null +++ b/src/audio_core/hle/common.h @@ -0,0 +1,36 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <algorithm> +#include <array> + +#include "common/common_types.h" + +namespace DSP { +namespace HLE { + +constexpr int num_sources = 24; +constexpr int samples_per_frame = 160; ///< Samples per audio frame at native sample rate + +/// The final output to the speakers is stereo. Preprocessing output in Source is also stereo. +using StereoFrame16 = std::array<std::array<s16, 2>, samples_per_frame>; + +/// The DSP is quadraphonic internally. +using QuadFrame32 = std::array<std::array<s32, 4>, samples_per_frame>; + +/** + * This performs the filter operation defined by FilterT::ProcessSample on the frame in-place. + * FilterT::ProcessSample is called sequentially on the samples. + */ +template<typename FrameT, typename FilterT> +void FilterFrame(FrameT& frame, FilterT& filter) { + std::transform(frame.begin(), frame.end(), frame.begin(), [&filter](const auto& sample) { + return filter.ProcessSample(sample); + }); +} + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/dsp.cpp b/src/audio_core/hle/dsp.cpp index c89356edc..0cdbdb06a 100644 --- a/src/audio_core/hle/dsp.cpp +++ b/src/audio_core/hle/dsp.cpp @@ -2,40 +2,81 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include <array> +#include <memory> + #include "audio_core/hle/dsp.h" #include "audio_core/hle/pipe.h" +#include "audio_core/hle/source.h" +#include "audio_core/sink.h" namespace DSP { namespace HLE { -SharedMemory g_region0; -SharedMemory g_region1; +std::array<SharedMemory, 2> g_regions; + +static size_t CurrentRegionIndex() { + // The region with the higher frame counter is chosen unless there is wraparound. + // This function only returns a 0 or 1. + + if (g_regions[0].frame_counter == 0xFFFFu && g_regions[1].frame_counter != 0xFFFEu) { + // Wraparound has occured. + return 1; + } + + if (g_regions[1].frame_counter == 0xFFFFu && g_regions[0].frame_counter != 0xFFFEu) { + // Wraparound has occured. + return 0; + } + + return (g_regions[0].frame_counter > g_regions[1].frame_counter) ? 0 : 1; +} + +static SharedMemory& ReadRegion() { + return g_regions[CurrentRegionIndex()]; +} + +static SharedMemory& WriteRegion() { + return g_regions[1 - CurrentRegionIndex()]; +} + +static std::array<Source, num_sources> sources = { + Source(0), Source(1), Source(2), Source(3), Source(4), Source(5), + Source(6), Source(7), Source(8), Source(9), Source(10), Source(11), + Source(12), Source(13), Source(14), Source(15), Source(16), Source(17), + Source(18), Source(19), Source(20), Source(21), Source(22), Source(23) +}; + +static std::unique_ptr<AudioCore::Sink> sink; void Init() { DSP::HLE::ResetPipes(); + for (auto& source : sources) { + source.Reset(); + } } void Shutdown() { } bool Tick() { - return true; -} + SharedMemory& read = ReadRegion(); + SharedMemory& write = WriteRegion(); -SharedMemory& CurrentRegion() { - // The region with the higher frame counter is chosen unless there is wraparound. + std::array<QuadFrame32, 3> intermediate_mixes = {}; - if (g_region0.frame_counter == 0xFFFFu && g_region1.frame_counter != 0xFFFEu) { - // Wraparound has occured. - return g_region1; + for (size_t i = 0; i < num_sources; i++) { + write.source_statuses.status[i] = sources[i].Tick(read.source_configurations.config[i], read.adpcm_coefficients.coeff[i]); + for (size_t mix = 0; mix < 3; mix++) { + sources[i].MixInto(intermediate_mixes[mix], mix); + } } - if (g_region1.frame_counter == 0xFFFFu && g_region0.frame_counter != 0xFFFEu) { - // Wraparound has occured. - return g_region0; - } + return true; +} - return (g_region0.frame_counter > g_region1.frame_counter) ? g_region0 : g_region1; +void SetSink(std::unique_ptr<AudioCore::Sink> sink_) { + sink = std::move(sink_); } } // namespace HLE diff --git a/src/audio_core/hle/dsp.h b/src/audio_core/hle/dsp.h index 376436c29..4459a5668 100644 --- a/src/audio_core/hle/dsp.h +++ b/src/audio_core/hle/dsp.h @@ -4,16 +4,22 @@ #pragma once +#include <array> #include <cstddef> +#include <memory> #include <type_traits> -#include "audio_core/audio_core.h" +#include "audio_core/hle/common.h" #include "common/bit_field.h" #include "common/common_funcs.h" #include "common/common_types.h" #include "common/swap.h" +namespace AudioCore { +class Sink; +} + namespace DSP { namespace HLE { @@ -30,10 +36,9 @@ namespace HLE { struct SharedMemory; constexpr VAddr region0_base = 0x1FF50000; -extern SharedMemory g_region0; - constexpr VAddr region1_base = 0x1FF70000; -extern SharedMemory g_region1; + +extern std::array<SharedMemory, 2> g_regions; /** * The DSP is native 16-bit. The DSP also appears to be big-endian. When reading 32-bit numbers from @@ -126,8 +131,11 @@ struct SourceConfiguration { union { u32_le dirty_raw; + BitField<0, 1, u32_le> format_dirty; + BitField<1, 1, u32_le> mono_or_stereo_dirty; BitField<2, 1, u32_le> adpcm_coefficients_dirty; BitField<3, 1, u32_le> partial_embedded_buffer_dirty; ///< Tends to be set when a looped buffer is queued. + BitField<4, 1, u32_le> partial_reset_flag; BitField<16, 1, u32_le> enable_dirty; BitField<17, 1, u32_le> interpolation_dirty; @@ -143,8 +151,7 @@ struct SourceConfiguration { BitField<27, 1, u32_le> gain_2_dirty; BitField<28, 1, u32_le> sync_dirty; BitField<29, 1, u32_le> reset_flag; - - BitField<31, 1, u32_le> embedded_buffer_dirty; + BitField<30, 1, u32_le> embedded_buffer_dirty; }; // Gain control @@ -162,9 +169,9 @@ struct SourceConfiguration { float_le rate_multiplier; enum class InterpolationMode : u8 { - None = 0, + Polyphase = 0, Linear = 1, - Polyphase = 2 + None = 2 }; InterpolationMode interpolation_mode; @@ -175,7 +182,8 @@ struct SourceConfiguration { /** * This is the simplest normalized first-order digital recursive filter. * The transfer function of this filter is: - * H(z) = b0 / (1 + a1 z^-1) + * H(z) = b0 / (1 - a1 z^-1) + * Note the feedbackward coefficient is negated. * Values are signed fixed point with 15 fractional bits. */ struct SimpleFilter { @@ -192,11 +200,11 @@ struct SourceConfiguration { * Values are signed fixed point with 14 fractional bits. */ struct BiquadFilter { - s16_le b0; - s16_le b1; - s16_le b2; - s16_le a1; s16_le a2; + s16_le a1; + s16_le b2; + s16_le b1; + s16_le b0; }; union { @@ -302,7 +310,7 @@ struct SourceConfiguration { u16_le buffer_id; }; - Configuration config[AudioCore::num_sources]; + Configuration config[num_sources]; }; ASSERT_DSP_STRUCT(SourceConfiguration::Configuration, 192); ASSERT_DSP_STRUCT(SourceConfiguration::Configuration::Buffer, 20); @@ -310,14 +318,14 @@ ASSERT_DSP_STRUCT(SourceConfiguration::Configuration::Buffer, 20); struct SourceStatus { struct Status { u8 is_enabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) - u8 previous_buffer_id_dirty; ///< Non-zero when previous_buffer_id changes + u8 current_buffer_id_dirty; ///< Non-zero when current_buffer_id changes u16_le sync; ///< Is set by the DSP to the value of SourceConfiguration::sync u32_dsp buffer_position; ///< Number of samples into the current buffer - u16_le previous_buffer_id; ///< Updated when a buffer finishes playing + u16_le current_buffer_id; ///< Updated when a buffer finishes playing INSERT_PADDING_DSPWORDS(1); }; - Status status[AudioCore::num_sources]; + Status status[num_sources]; }; ASSERT_DSP_STRUCT(SourceStatus::Status, 12); @@ -410,7 +418,7 @@ ASSERT_DSP_STRUCT(DspConfiguration::ReverbEffect, 52); struct AdpcmCoefficients { /// Coefficients are signed fixed point with 11 fractional bits. /// Each source has 16 coefficients associated with it. - s16_le coeff[AudioCore::num_sources][16]; + s16_le coeff[num_sources][16]; }; ASSERT_DSP_STRUCT(AdpcmCoefficients, 768); @@ -424,7 +432,7 @@ ASSERT_DSP_STRUCT(DspStatus, 32); /// Final mixed output in PCM16 stereo format, what you hear out of the speakers. /// When the application writes to this region it has no effect. struct FinalMixSamples { - s16_le pcm16[2 * AudioCore::samples_per_frame]; + s16_le pcm16[2 * samples_per_frame]; }; ASSERT_DSP_STRUCT(FinalMixSamples, 640); @@ -434,7 +442,7 @@ ASSERT_DSP_STRUCT(FinalMixSamples, 640); /// Values that exceed s16 range will be clipped by the DSP after further processing. struct IntermediateMixSamples { struct Samples { - s32_le pcm32[4][AudioCore::samples_per_frame]; ///< Little-endian as opposed to DSP middle-endian. + s32_le pcm32[4][samples_per_frame]; ///< Little-endian as opposed to DSP middle-endian. }; Samples mix1; @@ -532,8 +540,11 @@ void Shutdown(); */ bool Tick(); -/// Returns a mutable reference to the current region. Current region is selected based on the frame counter. -SharedMemory& CurrentRegion(); +/** + * Set the output sink. This must be called before calling Tick(). + * @param sink The sink to which audio will be output to. + */ +void SetSink(std::unique_ptr<AudioCore::Sink> sink); } // namespace HLE } // namespace DSP diff --git a/src/audio_core/hle/filter.cpp b/src/audio_core/hle/filter.cpp new file mode 100644 index 000000000..2c65ef026 --- /dev/null +++ b/src/audio_core/hle/filter.cpp @@ -0,0 +1,115 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <array> +#include <cstddef> + +#include "audio_core/hle/common.h" +#include "audio_core/hle/dsp.h" +#include "audio_core/hle/filter.h" + +#include "common/common_types.h" +#include "common/math_util.h" + +namespace DSP { +namespace HLE { + +void SourceFilters::Reset() { + Enable(false, false); +} + +void SourceFilters::Enable(bool simple, bool biquad) { + simple_filter_enabled = simple; + biquad_filter_enabled = biquad; + + if (!simple) + simple_filter.Reset(); + if (!biquad) + biquad_filter.Reset(); +} + +void SourceFilters::Configure(SourceConfiguration::Configuration::SimpleFilter config) { + simple_filter.Configure(config); +} + +void SourceFilters::Configure(SourceConfiguration::Configuration::BiquadFilter config) { + biquad_filter.Configure(config); +} + +void SourceFilters::ProcessFrame(StereoFrame16& frame) { + if (!simple_filter_enabled && !biquad_filter_enabled) + return; + + if (simple_filter_enabled) { + FilterFrame(frame, simple_filter); + } + + if (biquad_filter_enabled) { + FilterFrame(frame, biquad_filter); + } +} + +// SimpleFilter + +void SourceFilters::SimpleFilter::Reset() { + y1.fill(0); + // Configure as passthrough. + a1 = 0; + b0 = 1 << 15; +} + +void SourceFilters::SimpleFilter::Configure(SourceConfiguration::Configuration::SimpleFilter config) { + a1 = config.a1; + b0 = config.b0; +} + +std::array<s16, 2> SourceFilters::SimpleFilter::ProcessSample(const std::array<s16, 2>& x0) { + std::array<s16, 2> y0; + for (size_t i = 0; i < 2; i++) { + const s32 tmp = (b0 * x0[i] + a1 * y1[i]) >> 15; + y0[i] = MathUtil::Clamp(tmp, -32768, 32767); + } + + y1 = y0; + + return y0; +} + +// BiquadFilter + +void SourceFilters::BiquadFilter::Reset() { + x1.fill(0); + x2.fill(0); + y1.fill(0); + y2.fill(0); + // Configure as passthrough. + a1 = a2 = b1 = b2 = 0; + b0 = 1 << 14; +} + +void SourceFilters::BiquadFilter::Configure(SourceConfiguration::Configuration::BiquadFilter config) { + a1 = config.a1; + a2 = config.a2; + b0 = config.b0; + b1 = config.b1; + b2 = config.b2; +} + +std::array<s16, 2> SourceFilters::BiquadFilter::ProcessSample(const std::array<s16, 2>& x0) { + std::array<s16, 2> y0; + for (size_t i = 0; i < 2; i++) { + const s32 tmp = (b0 * x0[i] + b1 * x1[i] + b2 * x2[i] + a1 * y1[i] + a2 * y2[i]) >> 14; + y0[i] = MathUtil::Clamp(tmp, -32768, 32767); + } + + x2 = x1; + x1 = x0; + y2 = y1; + y1 = y0; + + return y0; +} + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/filter.h b/src/audio_core/hle/filter.h new file mode 100644 index 000000000..43d2035cd --- /dev/null +++ b/src/audio_core/hle/filter.h @@ -0,0 +1,113 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> + +#include "audio_core/hle/common.h" +#include "audio_core/hle/dsp.h" + +#include "common/common_types.h" + +namespace DSP { +namespace HLE { + +/// Preprocessing filters. There is an independent set of filters for each Source. +class SourceFilters final { +public: + SourceFilters() { Reset(); } + + /// Reset internal state. + void Reset(); + + /** + * Enable/Disable filters + * See also: SourceConfiguration::Configuration::simple_filter_enabled, + * SourceConfiguration::Configuration::biquad_filter_enabled. + * @param simple If true, enables the simple filter. If false, disables it. + * @param simple If true, enables the biquad filter. If false, disables it. + */ + void Enable(bool simple, bool biquad); + + /** + * Configure simple filter. + * @param config Configuration from DSP shared memory. + */ + void Configure(SourceConfiguration::Configuration::SimpleFilter config); + + /** + * Configure biquad filter. + * @param config Configuration from DSP shared memory. + */ + void Configure(SourceConfiguration::Configuration::BiquadFilter config); + + /** + * Processes a frame in-place. + * @param frame Audio samples to process. Modified in-place. + */ + void ProcessFrame(StereoFrame16& frame); + +private: + bool simple_filter_enabled; + bool biquad_filter_enabled; + + struct SimpleFilter { + SimpleFilter() { Reset(); } + + /// Resets internal state. + void Reset(); + + /** + * Configures this filter with application settings. + * @param config Configuration from DSP shared memory. + */ + void Configure(SourceConfiguration::Configuration::SimpleFilter config); + + /** + * Processes a single stereo PCM16 sample. + * @param x0 Input sample + * @return Output sample + */ + std::array<s16, 2> ProcessSample(const std::array<s16, 2>& x0); + + private: + // Configuration + s32 a1, b0; + // Internal state + std::array<s16, 2> y1; + } simple_filter; + + struct BiquadFilter { + BiquadFilter() { Reset(); } + + /// Resets internal state. + void Reset(); + + /** + * Configures this filter with application settings. + * @param config Configuration from DSP shared memory. + */ + void Configure(SourceConfiguration::Configuration::BiquadFilter config); + + /** + * Processes a single stereo PCM16 sample. + * @param x0 Input sample + * @return Output sample + */ + std::array<s16, 2> ProcessSample(const std::array<s16, 2>& x0); + + private: + // Configuration + s32 a1, a2, b0, b1, b2; + // Internal state + std::array<s16, 2> x1; + std::array<s16, 2> x2; + std::array<s16, 2> y1; + std::array<s16, 2> y2; + } biquad_filter; +}; + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/pipe.cpp b/src/audio_core/hle/pipe.cpp index 9381883b4..03280780f 100644 --- a/src/audio_core/hle/pipe.cpp +++ b/src/audio_core/hle/pipe.cpp @@ -12,12 +12,14 @@ #include "common/common_types.h" #include "common/logging/log.h" +#include "core/hle/service/dsp_dsp.h" + namespace DSP { namespace HLE { static DspState dsp_state = DspState::Off; -static std::array<std::vector<u8>, static_cast<size_t>(DspPipe::DspPipe_MAX)> pipe_data; +static std::array<std::vector<u8>, NUM_DSP_PIPE> pipe_data; void ResetPipes() { for (auto& data : pipe_data) { @@ -27,16 +29,18 @@ void ResetPipes() { } std::vector<u8> PipeRead(DspPipe pipe_number, u32 length) { - if (pipe_number >= DspPipe::DspPipe_MAX) { - LOG_ERROR(Audio_DSP, "pipe_number = %u invalid", pipe_number); + const size_t pipe_index = static_cast<size_t>(pipe_number); + + if (pipe_index >= NUM_DSP_PIPE) { + LOG_ERROR(Audio_DSP, "pipe_number = %zu invalid", pipe_index); return {}; } - std::vector<u8>& data = pipe_data[static_cast<size_t>(pipe_number)]; + std::vector<u8>& data = pipe_data[pipe_index]; if (length > data.size()) { - LOG_WARNING(Audio_DSP, "pipe_number = %u is out of data, application requested read of %u but %zu remain", - pipe_number, length, data.size()); + LOG_WARNING(Audio_DSP, "pipe_number = %zu is out of data, application requested read of %u but %zu remain", + pipe_index, length, data.size()); length = data.size(); } @@ -49,16 +53,20 @@ std::vector<u8> PipeRead(DspPipe pipe_number, u32 length) { } size_t GetPipeReadableSize(DspPipe pipe_number) { - if (pipe_number >= DspPipe::DspPipe_MAX) { - LOG_ERROR(Audio_DSP, "pipe_number = %u invalid", pipe_number); + const size_t pipe_index = static_cast<size_t>(pipe_number); + + if (pipe_index >= NUM_DSP_PIPE) { + LOG_ERROR(Audio_DSP, "pipe_number = %zu invalid", pipe_index); return 0; } - return pipe_data[static_cast<size_t>(pipe_number)].size(); + return pipe_data[pipe_index].size(); } static void WriteU16(DspPipe pipe_number, u16 value) { - std::vector<u8>& data = pipe_data[static_cast<size_t>(pipe_number)]; + const size_t pipe_index = static_cast<size_t>(pipe_number); + + std::vector<u8>& data = pipe_data.at(pipe_index); // Little endian data.emplace_back(value & 0xFF); data.emplace_back(value >> 8); @@ -91,6 +99,8 @@ static void AudioPipeWriteStructAddresses() { for (u16 addr : struct_addresses) { WriteU16(DspPipe::Audio, addr); } + // Signal that we have data on this pipe. + DSP_DSP::SignalPipeInterrupt(DspPipe::Audio); } void PipeWrite(DspPipe pipe_number, const std::vector<u8>& buffer) { @@ -145,7 +155,7 @@ void PipeWrite(DspPipe pipe_number, const std::vector<u8>& buffer) { return; } default: - LOG_CRITICAL(Audio_DSP, "pipe_number = %u unimplemented", pipe_number); + LOG_CRITICAL(Audio_DSP, "pipe_number = %zu unimplemented", static_cast<size_t>(pipe_number)); UNIMPLEMENTED(); return; } diff --git a/src/audio_core/hle/pipe.h b/src/audio_core/hle/pipe.h index 382d35e87..64d97f8ba 100644 --- a/src/audio_core/hle/pipe.h +++ b/src/audio_core/hle/pipe.h @@ -19,9 +19,9 @@ enum class DspPipe { Debug = 0, Dma = 1, Audio = 2, - Binary = 3, - DspPipe_MAX + Binary = 3 }; +constexpr size_t NUM_DSP_PIPE = 8; /** * Read a DSP pipe. diff --git a/src/audio_core/hle/source.cpp b/src/audio_core/hle/source.cpp new file mode 100644 index 000000000..daaf6e3f3 --- /dev/null +++ b/src/audio_core/hle/source.cpp @@ -0,0 +1,320 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <array> + +#include "audio_core/codec.h" +#include "audio_core/hle/common.h" +#include "audio_core/hle/source.h" +#include "audio_core/interpolate.h" + +#include "common/assert.h" +#include "common/logging/log.h" + +#include "core/memory.h" + +namespace DSP { +namespace HLE { + +SourceStatus::Status Source::Tick(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]) { + ParseConfig(config, adpcm_coeffs); + + if (state.enabled) { + GenerateFrame(); + } + + return GetCurrentStatus(); +} + +void Source::MixInto(QuadFrame32& dest, size_t intermediate_mix_id) const { + if (!state.enabled) + return; + + const std::array<float, 4>& gains = state.gain.at(intermediate_mix_id); + for (size_t samplei = 0; samplei < samples_per_frame; samplei++) { + // Conversion from stereo (current_frame) to quadraphonic (dest) occurs here. + dest[samplei][0] += static_cast<s32>(gains[0] * current_frame[samplei][0]); + dest[samplei][1] += static_cast<s32>(gains[1] * current_frame[samplei][1]); + dest[samplei][2] += static_cast<s32>(gains[2] * current_frame[samplei][0]); + dest[samplei][3] += static_cast<s32>(gains[3] * current_frame[samplei][1]); + } +} + +void Source::Reset() { + current_frame.fill({}); + state = {}; +} + +void Source::ParseConfig(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]) { + if (!config.dirty_raw) { + return; + } + + if (config.reset_flag) { + config.reset_flag.Assign(0); + Reset(); + LOG_TRACE(Audio_DSP, "source_id=%zu reset", source_id); + } + + if (config.partial_reset_flag) { + config.partial_reset_flag.Assign(0); + state.input_queue = std::priority_queue<Buffer, std::vector<Buffer>, BufferOrder>{}; + LOG_TRACE(Audio_DSP, "source_id=%zu partial_reset", source_id); + } + + if (config.enable_dirty) { + config.enable_dirty.Assign(0); + state.enabled = config.enable != 0; + LOG_TRACE(Audio_DSP, "source_id=%zu enable=%d", source_id, state.enabled); + } + + if (config.sync_dirty) { + config.sync_dirty.Assign(0); + state.sync = config.sync; + LOG_TRACE(Audio_DSP, "source_id=%zu sync=%u", source_id, state.sync); + } + + if (config.rate_multiplier_dirty) { + config.rate_multiplier_dirty.Assign(0); + state.rate_multiplier = config.rate_multiplier; + LOG_TRACE(Audio_DSP, "source_id=%zu rate=%f", source_id, state.rate_multiplier); + + if (state.rate_multiplier <= 0) { + LOG_ERROR(Audio_DSP, "Was given an invalid rate multiplier: source_id=%zu rate=%f", source_id, state.rate_multiplier); + state.rate_multiplier = 1.0f; + // Note: Actual firmware starts producing garbage if this occurs. + } + } + + if (config.adpcm_coefficients_dirty) { + config.adpcm_coefficients_dirty.Assign(0); + std::transform(adpcm_coeffs, adpcm_coeffs + state.adpcm_coeffs.size(), state.adpcm_coeffs.begin(), + [](const auto& coeff) { return static_cast<s16>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu adpcm update", source_id); + } + + if (config.gain_0_dirty) { + config.gain_0_dirty.Assign(0); + std::transform(config.gain[0], config.gain[0] + state.gain[0].size(), state.gain[0].begin(), + [](const auto& coeff) { return static_cast<float>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu gain 0 update", source_id); + } + + if (config.gain_1_dirty) { + config.gain_1_dirty.Assign(0); + std::transform(config.gain[1], config.gain[1] + state.gain[1].size(), state.gain[1].begin(), + [](const auto& coeff) { return static_cast<float>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu gain 1 update", source_id); + } + + if (config.gain_2_dirty) { + config.gain_2_dirty.Assign(0); + std::transform(config.gain[2], config.gain[2] + state.gain[2].size(), state.gain[2].begin(), + [](const auto& coeff) { return static_cast<float>(coeff); }); + LOG_TRACE(Audio_DSP, "source_id=%zu gain 2 update", source_id); + } + + if (config.filters_enabled_dirty) { + config.filters_enabled_dirty.Assign(0); + state.filters.Enable(config.simple_filter_enabled.ToBool(), config.biquad_filter_enabled.ToBool()); + LOG_TRACE(Audio_DSP, "source_id=%zu enable_simple=%hu enable_biquad=%hu", + source_id, config.simple_filter_enabled.Value(), config.biquad_filter_enabled.Value()); + } + + if (config.simple_filter_dirty) { + config.simple_filter_dirty.Assign(0); + state.filters.Configure(config.simple_filter); + LOG_TRACE(Audio_DSP, "source_id=%zu simple filter update"); + } + + if (config.biquad_filter_dirty) { + config.biquad_filter_dirty.Assign(0); + state.filters.Configure(config.biquad_filter); + LOG_TRACE(Audio_DSP, "source_id=%zu biquad filter update"); + } + + if (config.interpolation_dirty) { + config.interpolation_dirty.Assign(0); + state.interpolation_mode = config.interpolation_mode; + LOG_TRACE(Audio_DSP, "source_id=%zu interpolation_mode=%zu", source_id, static_cast<size_t>(state.interpolation_mode)); + } + + if (config.format_dirty || config.embedded_buffer_dirty) { + config.format_dirty.Assign(0); + state.format = config.format; + LOG_TRACE(Audio_DSP, "source_id=%zu format=%zu", source_id, static_cast<size_t>(state.format)); + } + + if (config.mono_or_stereo_dirty || config.embedded_buffer_dirty) { + config.mono_or_stereo_dirty.Assign(0); + state.mono_or_stereo = config.mono_or_stereo; + LOG_TRACE(Audio_DSP, "source_id=%zu mono_or_stereo=%zu", source_id, static_cast<size_t>(state.mono_or_stereo)); + } + + if (config.embedded_buffer_dirty) { + config.embedded_buffer_dirty.Assign(0); + state.input_queue.emplace(Buffer{ + config.physical_address, + config.length, + static_cast<u8>(config.adpcm_ps), + { config.adpcm_yn[0], config.adpcm_yn[1] }, + config.adpcm_dirty.ToBool(), + config.is_looping.ToBool(), + config.buffer_id, + state.mono_or_stereo, + state.format, + false + }); + LOG_TRACE(Audio_DSP, "enqueuing embedded addr=0x%08x len=%u id=%hu", config.physical_address, config.length, config.buffer_id); + } + + if (config.buffer_queue_dirty) { + config.buffer_queue_dirty.Assign(0); + for (size_t i = 0; i < 4; i++) { + if (config.buffers_dirty & (1 << i)) { + const auto& b = config.buffers[i]; + state.input_queue.emplace(Buffer{ + b.physical_address, + b.length, + static_cast<u8>(b.adpcm_ps), + { b.adpcm_yn[0], b.adpcm_yn[1] }, + b.adpcm_dirty != 0, + b.is_looping != 0, + b.buffer_id, + state.mono_or_stereo, + state.format, + true + }); + LOG_TRACE(Audio_DSP, "enqueuing queued %zu addr=0x%08x len=%u id=%hu", i, b.physical_address, b.length, b.buffer_id); + } + } + config.buffers_dirty = 0; + } + + if (config.dirty_raw) { + LOG_DEBUG(Audio_DSP, "source_id=%zu remaining_dirty=%x", source_id, config.dirty_raw); + } + + config.dirty_raw = 0; +} + +void Source::GenerateFrame() { + current_frame.fill({}); + + if (state.current_buffer.empty() && !DequeueBuffer()) { + state.enabled = false; + state.buffer_update = true; + state.current_buffer_id = 0; + return; + } + + size_t frame_position = 0; + + state.current_sample_number = state.next_sample_number; + while (frame_position < current_frame.size()) { + if (state.current_buffer.empty() && !DequeueBuffer()) { + break; + } + + const size_t size_to_copy = std::min(state.current_buffer.size(), current_frame.size() - frame_position); + + std::copy(state.current_buffer.begin(), state.current_buffer.begin() + size_to_copy, current_frame.begin() + frame_position); + state.current_buffer.erase(state.current_buffer.begin(), state.current_buffer.begin() + size_to_copy); + + frame_position += size_to_copy; + state.next_sample_number += static_cast<u32>(size_to_copy); + } + + state.filters.ProcessFrame(current_frame); +} + + +bool Source::DequeueBuffer() { + ASSERT_MSG(state.current_buffer.empty(), "Shouldn't dequeue; we still have data in current_buffer"); + + if (state.input_queue.empty()) + return false; + + const Buffer buf = state.input_queue.top(); + state.input_queue.pop(); + + if (buf.adpcm_dirty) { + state.adpcm_state.yn1 = buf.adpcm_yn[0]; + state.adpcm_state.yn2 = buf.adpcm_yn[1]; + } + + if (buf.is_looping) { + LOG_ERROR(Audio_DSP, "Looped buffers are unimplemented at the moment"); + } + + const u8* const memory = Memory::GetPhysicalPointer(buf.physical_address); + if (memory) { + const unsigned num_channels = buf.mono_or_stereo == MonoOrStereo::Stereo ? 2 : 1; + switch (buf.format) { + case Format::PCM8: + state.current_buffer = Codec::DecodePCM8(num_channels, memory, buf.length); + break; + case Format::PCM16: + state.current_buffer = Codec::DecodePCM16(num_channels, memory, buf.length); + break; + case Format::ADPCM: + DEBUG_ASSERT(num_channels == 1); + state.current_buffer = Codec::DecodeADPCM(memory, buf.length, state.adpcm_coeffs, state.adpcm_state); + break; + default: + UNIMPLEMENTED(); + break; + } + } else { + LOG_WARNING(Audio_DSP, "source_id=%zu buffer_id=%hu length=%u: Invalid physical address 0x%08X", + source_id, buf.buffer_id, buf.length, buf.physical_address); + state.current_buffer.clear(); + return true; + } + + switch (state.interpolation_mode) { + case InterpolationMode::None: + state.current_buffer = AudioInterp::None(state.interp_state, state.current_buffer, state.rate_multiplier); + break; + case InterpolationMode::Linear: + state.current_buffer = AudioInterp::Linear(state.interp_state, state.current_buffer, state.rate_multiplier); + break; + case InterpolationMode::Polyphase: + // TODO(merry): Implement polyphase interpolation + state.current_buffer = AudioInterp::Linear(state.interp_state, state.current_buffer, state.rate_multiplier); + break; + default: + UNIMPLEMENTED(); + break; + } + + state.current_sample_number = 0; + state.next_sample_number = 0; + state.current_buffer_id = buf.buffer_id; + state.buffer_update = buf.from_queue; + + LOG_TRACE(Audio_DSP, "source_id=%zu buffer_id=%hu from_queue=%s current_buffer.size()=%zu", + source_id, buf.buffer_id, buf.from_queue ? "true" : "false", state.current_buffer.size()); + return true; +} + +SourceStatus::Status Source::GetCurrentStatus() { + SourceStatus::Status ret; + + // Applications depend on the correct emulation of + // current_buffer_id_dirty and current_buffer_id to synchronise + // audio with video. + ret.is_enabled = state.enabled; + ret.current_buffer_id_dirty = state.buffer_update ? 1 : 0; + state.buffer_update = false; + ret.current_buffer_id = state.current_buffer_id; + ret.buffer_position = state.current_sample_number; + ret.sync = state.sync; + + return ret; +} + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/hle/source.h b/src/audio_core/hle/source.h new file mode 100644 index 000000000..7ee08d424 --- /dev/null +++ b/src/audio_core/hle/source.h @@ -0,0 +1,144 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <queue> +#include <vector> + +#include "audio_core/codec.h" +#include "audio_core/hle/common.h" +#include "audio_core/hle/dsp.h" +#include "audio_core/hle/filter.h" +#include "audio_core/interpolate.h" + +#include "common/common_types.h" + +namespace DSP { +namespace HLE { + +/** + * This module performs: + * - Buffer management + * - Decoding of buffers + * - Buffer resampling and interpolation + * - Per-source filtering (SimpleFilter, BiquadFilter) + * - Per-source gain + * - Other per-source processing + */ +class Source final { +public: + explicit Source(size_t source_id_) : source_id(source_id_) { + Reset(); + } + + /// Resets internal state. + void Reset(); + + /** + * This is called once every audio frame. This performs per-source processing every frame. + * @param config The new configuration we've got for this Source from the application. + * @param adpcm_coeffs ADPCM coefficients to use if config tells us to use them (may contain invalid values otherwise). + * @return The current status of this Source. This is given back to the emulated application via SharedMemory. + */ + SourceStatus::Status Tick(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]); + + /** + * Mix this source's output into dest, using the gains for the `intermediate_mix_id`-th intermediate mixer. + * @param dest The QuadFrame32 to mix into. + * @param intermediate_mix_id The id of the intermediate mix whose gains we are using. + */ + void MixInto(QuadFrame32& dest, size_t intermediate_mix_id) const; + +private: + const size_t source_id; + StereoFrame16 current_frame; + + using Format = SourceConfiguration::Configuration::Format; + using InterpolationMode = SourceConfiguration::Configuration::InterpolationMode; + using MonoOrStereo = SourceConfiguration::Configuration::MonoOrStereo; + + /// Internal representation of a buffer for our buffer queue + struct Buffer { + PAddr physical_address; + u32 length; + u8 adpcm_ps; + std::array<u16, 2> adpcm_yn; + bool adpcm_dirty; + bool is_looping; + u16 buffer_id; + + MonoOrStereo mono_or_stereo; + Format format; + + bool from_queue; + }; + + struct BufferOrder { + bool operator() (const Buffer& a, const Buffer& b) const { + // Lower buffer_id comes first. + return a.buffer_id > b.buffer_id; + } + }; + + struct { + + // State variables + + bool enabled = false; + u16 sync = 0; + + // Mixing + + std::array<std::array<float, 4>, 3> gain = {}; + + // Buffer queue + + std::priority_queue<Buffer, std::vector<Buffer>, BufferOrder> input_queue; + MonoOrStereo mono_or_stereo = MonoOrStereo::Mono; + Format format = Format::ADPCM; + + // Current buffer + + u32 current_sample_number = 0; + u32 next_sample_number = 0; + std::vector<std::array<s16, 2>> current_buffer; + + // buffer_id state + + bool buffer_update = false; + u32 current_buffer_id = 0; + + // Decoding state + + std::array<s16, 16> adpcm_coeffs = {}; + Codec::ADPCMState adpcm_state = {}; + + // Resampling state + + float rate_multiplier = 1.0; + InterpolationMode interpolation_mode = InterpolationMode::Polyphase; + AudioInterp::State interp_state = {}; + + // Filter state + + SourceFilters filters; + + } state; + + // Internal functions + + /// INTERNAL: Update our internal state based on the current config. + void ParseConfig(SourceConfiguration::Configuration& config, const s16_le (&adpcm_coeffs)[16]); + /// INTERNAL: Generate the current audio output for this frame based on our internal state. + void GenerateFrame(); + /// INTERNAL: Dequeues a buffer and does preprocessing on it (decoding, resampling). Puts it into current_buffer. + bool DequeueBuffer(); + /// INTERNAL: Generates a SourceStatus::Status based on our internal state. + SourceStatus::Status GetCurrentStatus(); +}; + +} // namespace HLE +} // namespace DSP diff --git a/src/audio_core/interpolate.cpp b/src/audio_core/interpolate.cpp new file mode 100644 index 000000000..fcd3aa066 --- /dev/null +++ b/src/audio_core/interpolate.cpp @@ -0,0 +1,85 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio_core/interpolate.h" + +#include "common/assert.h" +#include "common/math_util.h" + +namespace AudioInterp { + +// Calculations are done in fixed point with 24 fractional bits. +// (This is not verified. This was chosen for minimal error.) +constexpr u64 scale_factor = 1 << 24; +constexpr u64 scale_mask = scale_factor - 1; + +/// Here we step over the input in steps of rate_multiplier, until we consume all of the input. +/// Three adjacent samples are passed to fn each step. +template <typename Function> +static StereoBuffer16 StepOverSamples(State& state, const StereoBuffer16& input, float rate_multiplier, Function fn) { + ASSERT(rate_multiplier > 0); + + if (input.size() < 2) + return {}; + + StereoBuffer16 output; + output.reserve(static_cast<size_t>(input.size() / rate_multiplier)); + + u64 step_size = static_cast<u64>(rate_multiplier * scale_factor); + + u64 fposition = 0; + const u64 max_fposition = input.size() * scale_factor; + + while (fposition < 1 * scale_factor) { + u64 fraction = fposition & scale_mask; + + output.push_back(fn(fraction, state.xn2, state.xn1, input[0])); + + fposition += step_size; + } + + while (fposition < 2 * scale_factor) { + u64 fraction = fposition & scale_mask; + + output.push_back(fn(fraction, state.xn1, input[0], input[1])); + + fposition += step_size; + } + + while (fposition < max_fposition) { + u64 fraction = fposition & scale_mask; + + size_t index = static_cast<size_t>(fposition / scale_factor); + output.push_back(fn(fraction, input[index - 2], input[index - 1], input[index])); + + fposition += step_size; + } + + state.xn2 = input[input.size() - 2]; + state.xn1 = input[input.size() - 1]; + + return output; +} + +StereoBuffer16 None(State& state, const StereoBuffer16& input, float rate_multiplier) { + return StepOverSamples(state, input, rate_multiplier, [](u64 fraction, const auto& x0, const auto& x1, const auto& x2) { + return x0; + }); +} + +StereoBuffer16 Linear(State& state, const StereoBuffer16& input, float rate_multiplier) { + // Note on accuracy: Some values that this produces are +/- 1 from the actual firmware. + return StepOverSamples(state, input, rate_multiplier, [](u64 fraction, const auto& x0, const auto& x1, const auto& x2) { + // This is a saturated subtraction. (Verified by black-box fuzzing.) + s64 delta0 = MathUtil::Clamp<s64>(x1[0] - x0[0], -32768, 32767); + s64 delta1 = MathUtil::Clamp<s64>(x1[1] - x0[1], -32768, 32767); + + return std::array<s16, 2> { + static_cast<s16>(x0[0] + fraction * delta0 / scale_factor), + static_cast<s16>(x0[1] + fraction * delta1 / scale_factor) + }; + }); +} + +} // namespace AudioInterp diff --git a/src/audio_core/interpolate.h b/src/audio_core/interpolate.h new file mode 100644 index 000000000..a4c0a453d --- /dev/null +++ b/src/audio_core/interpolate.h @@ -0,0 +1,41 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <vector> + +#include "common/common_types.h" + +namespace AudioInterp { + +/// A variable length buffer of signed PCM16 stereo samples. +using StereoBuffer16 = std::vector<std::array<s16, 2>>; + +struct State { + // Two historical samples. + std::array<s16, 2> xn1 = {}; ///< x[n-1] + std::array<s16, 2> xn2 = {}; ///< x[n-2] +}; + +/** + * No interpolation. This is equivalent to a zero-order hold. There is a two-sample predelay. + * @param input Input buffer. + * @param rate_multiplier Stretch factor. Must be a positive non-zero value. + * rate_multiplier > 1.0 performs decimation and rate_multipler < 1.0 performs upsampling. + * @return The resampled audio buffer. + */ +StereoBuffer16 None(State& state, const StereoBuffer16& input, float rate_multiplier); + +/** + * Linear interpolation. This is equivalent to a first-order hold. There is a two-sample predelay. + * @param input Input buffer. + * @param rate_multiplier Stretch factor. Must be a positive non-zero value. + * rate_multiplier > 1.0 performs decimation and rate_multipler < 1.0 performs upsampling. + * @return The resampled audio buffer. + */ +StereoBuffer16 Linear(State& state, const StereoBuffer16& input, float rate_multiplier); + +} // namespace AudioInterp diff --git a/src/audio_core/null_sink.h b/src/audio_core/null_sink.h new file mode 100644 index 000000000..faf0ee4e1 --- /dev/null +++ b/src/audio_core/null_sink.h @@ -0,0 +1,29 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <cstddef> + +#include "audio_core/audio_core.h" +#include "audio_core/sink.h" + +namespace AudioCore { + +class NullSink final : public Sink { +public: + ~NullSink() override = default; + + unsigned int GetNativeSampleRate() const override { + return native_sample_rate; + } + + void EnqueueSamples(const std::vector<s16>&) override {} + + size_t SamplesInQueue() const override { + return 0; + } +}; + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp new file mode 100644 index 000000000..d2cc74103 --- /dev/null +++ b/src/audio_core/sink_details.cpp @@ -0,0 +1,18 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <memory> +#include <vector> + +#include "audio_core/null_sink.h" +#include "audio_core/sink_details.h" + +namespace AudioCore { + +// g_sink_details is ordered in terms of desirability, with the best choice at the top. +const std::vector<SinkDetails> g_sink_details = { + { "null", []() { return std::make_unique<NullSink>(); } }, +}; + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h new file mode 100644 index 000000000..4b30cf835 --- /dev/null +++ b/src/audio_core/sink_details.h @@ -0,0 +1,27 @@ +// Copyright 2016 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <functional> +#include <memory> +#include <vector> + +namespace AudioCore { + +class Sink; + +struct SinkDetails { + SinkDetails(const char* id_, std::function<std::unique_ptr<Sink>()> factory_) + : id(id_), factory(factory_) {} + + /// Name for this sink. + const char* id; + /// A method to call to construct an instance of this type of sink. + std::function<std::unique_ptr<Sink>()> factory; +}; + +extern const std::vector<SinkDetails> g_sink_details; + +} // namespace AudioCore |
