diff --git a/CMakeLists.txt b/CMakeLists.txt index e638c47aab..5f25235bb5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2358,6 +2358,8 @@ add_library(${CoreLibName} ${CoreLinkType} Core/HW/Camera.h Core/HW/Display.cpp Core/HW/Display.h + Core/HW/GranularMixer.cpp + Core/HW/GranularMixer.h Core/HW/MediaEngine.cpp Core/HW/MediaEngine.h Core/HW/MpegDemux.cpp diff --git a/Core/Config.cpp b/Core/Config.cpp index 6c9c1830e1..511e2fb370 100644 --- a/Core/Config.cpp +++ b/Core/Config.cpp @@ -784,6 +784,9 @@ static const ConfigSetting soundSettings[] = { ConfigSetting("ExtraAudioBuffering", &g_Config.bExtraAudioBuffering, false, CfgFlag::DEFAULT), ConfigSetting("AudioBufferSize", &g_Config.iSDLAudioBufferSize, 256, CfgFlag::DEFAULT), + ConfigSetting("FillAudioGaps", &g_Config.bFillAudioGaps, true, CfgFlag::DEFAULT), + ConfigSetting("AudioSyncMode", &g_Config.iAudioSyncMode, (int)AudioSyncMode::CLASSIC_PITCH, CfgFlag::DEFAULT), + // Legacy volume settings, these get auto upgraded through default handlers on the new settings. NOTE: Must be before the new ones in the order here. // The default settings here are still relevant, they will get propagated into the new ones. ConfigSetting("GlobalVolume", &g_Config.iLegacyGameVolume, VOLUME_FULL, CfgFlag::PER_GAME | CfgFlag::DONT_SAVE), diff --git a/Core/Config.h b/Core/Config.h index 1852e6c9bc..efd69c9288 100644 --- a/Core/Config.h +++ b/Core/Config.h @@ -289,6 +289,9 @@ public: // Sound bool bEnableSound; int iSDLAudioBufferSize; + int iAudioBufferSize; + bool bFillAudioGaps; + int iAudioSyncMode; // Legacy volume settings, 0-10. These get auto-upgraded and should not be used. int iLegacyGameVolume; diff --git a/Core/ConfigValues.h b/Core/ConfigValues.h index ce6f30c369..efea63c002 100644 --- a/Core/ConfigValues.h +++ b/Core/ConfigValues.h @@ -117,6 +117,11 @@ enum class DepthRasterMode { FORCE_ON = 3, }; +enum class AudioSyncMode { + GRANULAR = 0, + CLASSIC_PITCH = 1, +}; + enum class RestoreSettingsBits : int { SETTINGS = 1, CONTROLS = 2, diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 41b0645c51..abe294c4a8 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -491,6 +491,7 @@ + @@ -1050,6 +1051,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 4dfaa70ccb..b53609f8b3 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -1390,6 +1390,9 @@ Ext + + HW + @@ -2244,6 +2247,9 @@ Debugger + + HW + @@ -2272,4 +2278,4 @@ Ext - \ No newline at end of file + diff --git a/Core/HW/GranularMixer.cpp b/Core/HW/GranularMixer.cpp new file mode 100644 index 0000000000..b12e32dfaf --- /dev/null +++ b/Core/HW/GranularMixer.cpp @@ -0,0 +1,307 @@ +// Copyright 2008 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "Core/HW/GranularMixer.h" + +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Common/Log.h" +#include "Common/Math/math_util.h" +#include "Common/Swap.h" +#include "Core/HW/Display.h" +#include "Core/Core.h" +#include "Core/System.h" +#include "Core/Util/AudioFormat.h" // for clamp_u16 + +// Something like a gaussian. +static const float g_GranuleWindow[256] = { + 0.0000016272f, 0.0000050749f, 0.0000113187f, 0.0000216492f, 0.0000377350f, 0.0000616906f, + 0.0000961509f, 0.0001443499f, 0.0002102045f, 0.0002984010f, 0.0004144844f, 0.0005649486f, + 0.0007573262f, 0.0010002765f, 0.0013036694f, 0.0016786636f, 0.0021377783f, 0.0026949534f, + 0.0033656000f, 0.0041666352f, 0.0051165029f, 0.0062351752f, 0.0075441359f, 0.0090663409f, + 0.0108261579f, 0.0128492811f, 0.0151626215f, 0.0177941726f, 0.0207728499f, 0.0241283062f, + 0.0278907219f, 0.0320905724f, 0.0367583739f, 0.0419244083f, 0.0476184323f, 0.0538693708f, + 0.0607049996f, 0.0681516192f, 0.0762337261f, 0.0849736833f, 0.0943913952f, 0.1045039915f, + 0.1153255250f, 0.1268666867f, 0.1391345431f, 0.1521323012f, 0.1658591025f, 0.1803098534f, + 0.1954750915f, 0.2113408944f, 0.2278888303f, 0.2450959552f, 0.2629348550f, 0.2813737361f, + 0.3003765625f, 0.3199032396f, 0.3399098438f, 0.3603488941f, 0.3811696664f, 0.4023185434f, + 0.4237393998f, 0.4453740162f, 0.4671625177f, 0.4890438330f, 0.5109561670f, 0.5328374823f, + 0.5546259838f, 0.5762606002f, 0.5976814566f, 0.6188303336f, 0.6396511059f, 0.6600901562f, + 0.6800967604f, 0.6996234375f, 0.7186262639f, 0.7370651450f, 0.7549040448f, 0.7721111697f, + 0.7886591056f, 0.8045249085f, 0.8196901466f, 0.8341408975f, 0.8478676988f, 0.8608654569f, + 0.8731333133f, 0.8846744750f, 0.8954960085f, 0.9056086048f, 0.9150263167f, 0.9237662739f, + 0.9318483808f, 0.9392950004f, 0.9461306292f, 0.9523815677f, 0.9580755917f, 0.9632416261f, + 0.9679094276f, 0.9721092781f, 0.9758716938f, 0.9792271501f, 0.9822058274f, 0.9848373785f, + 0.9871507189f, 0.9891738421f, 0.9909336591f, 0.9924558641f, 0.9937648248f, 0.9948834971f, + 0.9958333648f, 0.9966344000f, 0.9973050466f, 0.9978622217f, 0.9983213364f, 0.9986963306f, + 0.9989997235f, 0.9992426738f, 0.9994350514f, 0.9995855156f, 0.9997015990f, 0.9997897955f, + 0.9998556501f, 0.9999038491f, 0.9999383094f, 0.9999622650f, 0.9999783508f, 0.9999886813f, + 0.9999949251f, 0.9999983728f, 0.9999983728f, 0.9999949251f, 0.9999886813f, 0.9999783508f, + 0.9999622650f, 0.9999383094f, 0.9999038491f, 0.9998556501f, 0.9997897955f, 0.9997015990f, + 0.9995855156f, 0.9994350514f, 0.9992426738f, 0.9989997235f, 0.9986963306f, 0.9983213364f, + 0.9978622217f, 0.9973050466f, 0.9966344000f, 0.9958333648f, 0.9948834971f, 0.9937648248f, + 0.9924558641f, 0.9909336591f, 0.9891738421f, 0.9871507189f, 0.9848373785f, 0.9822058274f, + 0.9792271501f, 0.9758716938f, 0.9721092781f, 0.9679094276f, 0.9632416261f, 0.9580755917f, + 0.9523815677f, 0.9461306292f, 0.9392950004f, 0.9318483808f, 0.9237662739f, 0.9150263167f, + 0.9056086048f, 0.8954960085f, 0.8846744750f, 0.8731333133f, 0.8608654569f, 0.8478676988f, + 0.8341408975f, 0.8196901466f, 0.8045249085f, 0.7886591056f, 0.7721111697f, 0.7549040448f, + 0.7370651450f, 0.7186262639f, 0.6996234375f, 0.6800967604f, 0.6600901562f, 0.6396511059f, + 0.6188303336f, 0.5976814566f, 0.5762606002f, 0.5546259838f, 0.5328374823f, 0.5109561670f, + 0.4890438330f, 0.4671625177f, 0.4453740162f, 0.4237393998f, 0.4023185434f, 0.3811696664f, + 0.3603488941f, 0.3399098438f, 0.3199032396f, 0.3003765625f, 0.2813737361f, 0.2629348550f, + 0.2450959552f, 0.2278888303f, 0.2113408944f, 0.1954750915f, 0.1803098534f, 0.1658591025f, + 0.1521323012f, 0.1391345431f, 0.1268666867f, 0.1153255250f, 0.1045039915f, 0.0943913952f, + 0.0849736833f, 0.0762337261f, 0.0681516192f, 0.0607049996f, 0.0538693708f, 0.0476184323f, + 0.0419244083f, 0.0367583739f, 0.0320905724f, 0.0278907219f, 0.0241283062f, 0.0207728499f, + 0.0177941726f, 0.0151626215f, 0.0128492811f, 0.0108261579f, 0.0090663409f, 0.0075441359f, + 0.0062351752f, 0.0051165029f, 0.0041666352f, 0.0033656000f, 0.0026949534f, 0.0021377783f, + 0.0016786636f, 0.0013036694f, 0.0010002765f, 0.0007573262f, 0.0005649486f, 0.0004144844f, + 0.0002984010f, 0.0002102045f, 0.0001443499f, 0.0000961509f, 0.0000616906f, 0.0000377350f, + 0.0000216492f, 0.0000113187f, 0.0000050749f, 0.0000016272f +}; + +inline s16 clampfloat_s16(float f) { + if (f <= -32767.0f) return -32767; + if (f >= 32767.0f) return 32767; + return (s16)f; +} + +GranularMixer::GranularMixer() { + INFO_LOG(Log::Audio, "Mixer is initialized"); +} + +// Executed from sound stream thread +void GranularMixer::Mix(s16 *samples, u32 num_samples, int outSampleRate, float fpsEstimate) { + _dbg_assert_(samples); + if (!samples) + return; + memset(samples, 0, num_samples * 2 * sizeof(s16)); + frameTimeEstimate_ = 1.0f / fpsEstimate; + + smoothedReadSize_ = smoothedReadSize_ == 0 ? num_samples : (smoothedReadSize_ * 0.95f + num_samples * 0.05f); + + constexpr u32 INDEX_HALF = 0x80000000; + constexpr double FADE_IN_RC = 0.008; + constexpr double FADE_OUT_RC = 0.064; + + // We need at least a double because the index jump has 24 bits of fractional precision. + const double out_sample_rate = outSampleRate; + double inSampleRate = 44100; + + const double emulation_speed = 1.0f; // TODO: Change when we're in slow-motion mode etc. + if (0 < emulation_speed && emulation_speed != 1.0) + inSampleRate *= emulation_speed; + + const double base = static_cast(1 << GRANULE_FRAC_BITS); + const u32 index_jump = std::lround(base * inSampleRate / out_sample_rate); + + // These fade in / out multiplier are tuned to match a constant + // fade speed regardless of the input or the output sample rate. + const float fade_in_mul = -std::expm1(-1.0 / (out_sample_rate * FADE_IN_RC)); + const float fade_out_mul = -std::expm1(-1.0 / (out_sample_rate * FADE_OUT_RC)); + + // Calculate the ideal length of the granule queue. + // NOTE: We must have enough room here for 20fps games, generating all their audio + // in a burst each frame (since we can't force real clock sync). That means 16*3 = 48 or rather 50ms. + // However, in case of faster framerates, we should apply some pressure to reduce this. And if real clock sync + // is on, we should also be able to get away with a shorter buffer here. + // const u32 buffer_size_ms = frameTimeEstimate_ * 44100.0f; + const u32 buffer_size_samples = smoothedReadSize_ * 4 + std::llround(frameTimeEstimate_ * inSampleRate); + queuedSamplesTarget_ = buffer_size_samples; + + // Limit the possible queue sizes to any number between 4 and 64. + const u32 buffer_size_granules = + std::clamp((buffer_size_samples) / (GRANULE_SIZE >> 1), static_cast(4), + static_cast(MAX_GRANULE_QUEUE_SIZE)); + + if (buffer_size_granules != m_granule_queue_size.load(std::memory_order_relaxed)) { + INFO_LOG(Log::Audio, "Granule buffer size changed to %d", buffer_size_granules); + } + + m_granule_queue_size.store(buffer_size_granules, std::memory_order_relaxed); + + int actualQueueSize = m_queue_head - m_queue_tail; + if (smoothedQueueSize_ == 0) { + smoothedQueueSize_ = actualQueueSize; + } else { + constexpr float factor = 0.95f; + smoothedQueueSize_ = factor * smoothedQueueSize_ + (1.0f - factor) * (float)actualQueueSize; + } + if (actualQueueSize < queuedGranulesMin_) { + queuedGranulesMin_ = actualQueueSize; + } + if (actualQueueSize > queuedGranulesMax_) { + queuedGranulesMax_ = actualQueueSize; + } + + // TODO: The performance of this could be greatly enhanced with SIMD but it won't be easy + // due to wrapping of various buffers. + bool queue_looping = m_queue_looping.load(std::memory_order_relaxed); + while (num_samples-- > 0) { + // The indexes for the front and back buffers are offset by 50% of the granule size. + // We use the modular nature of 32-bit integers to wrap around the granule size. + m_current_index += index_jump; + const u32 front_index = m_current_index; + const u32 back_index = m_current_index + INDEX_HALF; + + // If either index is less than the index jump, that means we reached + // the end of the of the buffer and need to load the next granule. + if (front_index < index_jump) + Dequeue(&m_front); + else if (back_index < index_jump) + Dequeue(&m_back); + + // The Granules are pre-windowed, so we can just add them together. A bit of accidental wrapping doesn't matter + // either since the tails are so weak. + const u32 ft = front_index >> GRANULE_FRAC_BITS; + const u32 bt = back_index >> GRANULE_FRAC_BITS; + const StereoPair s0 = m_front[(ft - 2) & GRANULE_MASK] + m_back[(bt - 2) & GRANULE_MASK]; + const StereoPair s1 = m_front[(ft - 1) & GRANULE_MASK] + m_back[(bt - 1) & GRANULE_MASK]; + const StereoPair s2 = m_front[(ft + 0) & GRANULE_MASK] + m_back[(bt + 0) & GRANULE_MASK]; + const StereoPair s3 = m_front[(ft + 1) & GRANULE_MASK] + m_back[(bt + 1) & GRANULE_MASK]; + const StereoPair s4 = m_front[(ft + 2) & GRANULE_MASK] + m_back[(bt + 2) & GRANULE_MASK]; + const StereoPair s5 = m_front[(ft + 3) & GRANULE_MASK] + m_back[(bt + 3) & GRANULE_MASK]; + + // Probably an overkill interpolator, but let's go with it for now. + // Polynomial Interpolators for High-Quality Resampling of + // Over Sampled Audio by Olli Niemitalo, October 2001. + // Page 43 -- 6-point, 3rd-order Hermite: + // https://yehar.com/blog/wp-content/uploads/2009/08/deip.pdf + const u32 t_frac = m_current_index & ((1 << GRANULE_FRAC_BITS) - 1); + const float t1 = t_frac / static_cast(1 << GRANULE_FRAC_BITS); + const float t2 = t1 * t1; + const float t3 = t2 * t1; + StereoPair sample = ( + s0 * ((+0.0f + 1.0f * t1 - 2.0f * t2 + 1.0f * t3) * (1.0f / 12.0f)) + + s1 * ((+0.0f - 8.0f * t1 + 15.0f * t2 - 7.0f * t3) * (1.0f / 12.0f)) + + s2 * ((+3.0f + 0.0f * t1 - 7.0f * t2 + 4.0f * t3) * (1.0f / 3.0f)) + + s3 * ((+0.0f + 2.0f * t1 + 5.0f * t2 - 4.0f * t3) * (1.0f / 3.0f)) + + s4 * ((+0.0f - 1.0f * t1 - 6.0f * t2 + 7.0f * t3) * (1.0f / 12.0f)) + + s5 * ((+0.0f + 0.0f * t1 + 1.0f * t2 - 1.0f * t3) * (1.0f / 12.0f)) + ); + + // Update the looping flag occasionally. + if (!(num_samples & 31)) { + queue_looping = m_queue_looping.load(std::memory_order_relaxed); + } + + // Apply Fade In / Fade Out depending on if we are looping + if (queue_looping) + m_fade_volume += fade_out_mul * (0.0f - m_fade_volume); + else + m_fade_volume += fade_in_mul * (1.0f - m_fade_volume); + + samples[0] = (int16_t)clamp_value(sample.l * m_fade_volume, -32767.0f, 32767.0f); + samples[1] = (int16_t)clamp_value(sample.r * m_fade_volume, -32767.0f, 32767.0f); + + samples += 2; + } +} + +void GranularMixer::PushSamples(const s32 *samples, u32 num_samples, float volume) { + // TODO: This can be massively sped up. Although hardly likely to be a bottleneck. + while (num_samples-- > 0) { + const s16 l = clampfloat_s16(samples[0] * volume); + const s16 r = clampfloat_s16(samples[1] * volume); + samples += 2; + + m_next_buffer[m_next_buffer_index] = StereoPair(l, r); + m_next_buffer_index = (m_next_buffer_index + 1) & GRANULE_MASK; + + // The granules overlap by 50%, so we need to enqueue the + // next buffer every time we fill half of the samples. + if (m_next_buffer_index == 0 || m_next_buffer_index == m_next_buffer.size() / 2) { + Enqueue(); + } + } +} + +void GranularMixer::Enqueue() { + const u32 head = m_queue_head.load(std::memory_order_acquire); + + // Check if we run out of space in the circular queue. (rare) + u32 next_head = head + 1; + if ((next_head & GRANULE_QUEUE_MASK) == (m_queue_tail.load(std::memory_order_acquire) & GRANULE_QUEUE_MASK)) { + WARN_LOG(Log::Audio, + "Granule Queue has completely filled and audio samples are being dropped. " + "This should not happen unless the audio backend has stopped requesting audio."); + return; + } + + // The compiler (at least MSVC) fails at optimizing this loop using SIMD instructions. + const u32 start_index = m_next_buffer_index; + + const u32 maskedHead = head & GRANULE_QUEUE_MASK; + for (u32 i = 0; i < GRANULE_SIZE; ++i) { + m_queue[maskedHead][i] = m_next_buffer[(i + start_index) & GRANULE_MASK] * g_GranuleWindow[i]; + } + + m_queue_head.store(next_head, std::memory_order_release); + m_queue_looping.store(false, std::memory_order_relaxed); +} + +void GranularMixer::Dequeue(Granule *granule) { + const u32 granule_queue_size = m_granule_queue_size.load(std::memory_order_relaxed); + const u32 head = m_queue_head.load(std::memory_order_acquire); + u32 tail = m_queue_tail.load(std::memory_order_acquire); + + // Checks to see if the queue has gotten too long. + if ((head - tail) > granule_queue_size) { + // Jump the playhead to half the queue size behind the head. + const u32 gap = (granule_queue_size >> 1) + 1; + tail = (head - gap); + overruns_++; + } + + // Checks to see if the queue is empty. + u32 next_tail = tail + 1; + + bool looping = m_queue_looping.load(); + + /*if (!looping && !smoothedQueueSize_ < granule_queue_size / 2) { + // Repeat a single block occasionally to make sure we have a reasonably sized queue. + next_tail = tail; + } else*/ if (next_tail == head) { + // Only fill gaps when running to prevent stutter on pause. + CoreState state = coreState; + const bool is_running = state == CORE_RUNNING_CPU || state == CORE_RUNNING_GE; + if (g_Config.bFillAudioGaps && is_running) { + // Jump the playhead to half the queue size behind the head. + // This will repeat a few past granules I guess? They still contain sensible data. + // This provides smoother audio playback than suddenly stopping. + const u32 gap = std::max(2, granule_queue_size >> 1) - 1; + next_tail = head - gap; + underruns_++; + m_queue_looping.store(true, std::memory_order_relaxed); + } else { + // Send a zero granule. + std::fill(granule->begin(), granule->end(), StereoPair{ 0.0f, 0.0f }); + m_queue_looping.store(false, std::memory_order_relaxed); + return; + } + } + + *granule = m_queue[tail & GRANULE_QUEUE_MASK]; + m_queue_tail.store(next_tail, std::memory_order_release); +} + +void GranularMixer::GetStats(GranularStats *stats) { + stats->queuedGranulesMin = queuedGranulesMin_; + stats->queuedGranulesMax = queuedGranulesMax_; + stats->smoothedQueuedGranules = smoothedQueueSize_; + stats->targetQueueSize = m_granule_queue_size.load(std::memory_order_relaxed); + stats->maxQueuedGranules = MAX_GRANULE_QUEUE_SIZE; + stats->fadeVolume = m_fade_volume; + stats->looping = m_queue_looping; + stats->overruns = overruns_; + stats->underruns = underruns_; + stats->smoothedReadSize = smoothedReadSize_; + stats->frameTimeEstimate = frameTimeEstimate_; + stats->queuedSamplesTarget = queuedSamplesTarget_; + queuedGranulesMin_ = 10000; + queuedGranulesMax_ = 0; +} diff --git a/Core/HW/GranularMixer.h b/Core/HW/GranularMixer.h new file mode 100644 index 0000000000..05daba77d1 --- /dev/null +++ b/Core/HW/GranularMixer.h @@ -0,0 +1,116 @@ +// Copyright 2009 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include + +#include "Common/CommonTypes.h" +#include "Core/Config.h" + +class PointerWrap; + +// Replacement for std::countr_one from a later version of C++ +constexpr u32 countr_one_replacement(u32 x) { + u32 count = 0; + while (x & 1) { + ++count; + x >>= 1; + } + return count; +} + +struct GranularStats { + int queuedGranulesMin; + int queuedGranulesMax; + int queuedSamplesTarget; + float smoothedQueuedGranules; + int targetQueueSize; + int maxQueuedGranules; + float fadeVolume; + bool looping; + int overruns; + int underruns; + float smoothedReadSize; + float frameTimeEstimate; +}; + +class GranularMixer final { +public: + explicit GranularMixer(); + + // Called from audio threads + void Mix(s16 *samples, u32 numSamples, int outSampleRate, float vpsEstimate); + + // Called from emulation thread + void PushSamples(const s32 *samples, u32 num_samples, float volume); + + void GetStats(GranularStats *stats); + + static constexpr u32 GRANULE_SIZE = 256; + +private: + static constexpr std::size_t MAX_GRANULE_QUEUE_SIZE = 128; + static constexpr std::size_t GRANULE_QUEUE_MASK = MAX_GRANULE_QUEUE_SIZE - 1; + + struct StereoPair final { + float l = 0.f; + float r = 0.f; + + constexpr StereoPair() = default; + constexpr StereoPair(const StereoPair&) = default; + constexpr StereoPair& operator=(const StereoPair&) = default; + constexpr StereoPair(StereoPair&&) = default; + constexpr StereoPair& operator=(StereoPair&&) = default; + + constexpr StereoPair(float mono) : l(mono), r(mono) {} + constexpr StereoPair(float left, float right) : l(left), r(right) {} + constexpr StereoPair(s16 left, s16 right) : l(left), r(right) {} + + StereoPair operator+(const StereoPair& other) const { + return StereoPair(l + other.l, r + other.r); + } + + StereoPair operator*(float f) const { + return StereoPair(l * f, r * f); + } + }; + + static constexpr u32 GRANULE_OVERLAP = GRANULE_SIZE / 2; + static constexpr u32 GRANULE_MASK = GRANULE_SIZE - 1; + static constexpr u32 GRANULE_BITS = countr_one_replacement(GRANULE_MASK); + static constexpr u32 GRANULE_FRAC_BITS = 32 - GRANULE_BITS; + + using Granule = std::array; + + Granule m_next_buffer{}; + u32 m_next_buffer_index = 0; + + u32 m_current_index = 0; + Granule m_front; + Granule m_back; + + std::atomic m_granule_queue_size{ 20 }; + float smoothedQueueSize_ = 0.0f; + Granule m_queue[MAX_GRANULE_QUEUE_SIZE]; + + // These are permitted to grow indefinitely and masked on use. + std::atomic m_queue_head{}; + std::atomic m_queue_tail{}; + std::atomic m_queue_looping{}; + + float m_fade_volume = 1.0; + int underruns_ = 0; + int overruns_ = 0; + int queuedGranulesMin_ = 10000; + int queuedGranulesMax_ = 0; + int queuedSamplesTarget_ = 0; + float smoothedReadSize_ = 0.0f; + float frameTimeEstimate_ = 0.0f; + + void Enqueue(); + void Dequeue(Granule* granule); +}; diff --git a/Core/HW/StereoResampler.cpp b/Core/HW/StereoResampler.cpp index f5e4be4054..ec44ae1971 100644 --- a/Core/HW/StereoResampler.cpp +++ b/Core/HW/StereoResampler.cpp @@ -46,7 +46,7 @@ #include "Core/Config.h" #include "Core/ConfigValues.h" #include "Core/HW/StereoResampler.h" -#include "Core/Util/AudioFormat.h" // for clamp_u8 +#include "Core/Util/AudioFormat.h" // for clamp_u16 #include "Core/System.h" StereoResampler::StereoResampler() noexcept diff --git a/GPU/Common/PresentationCommon.cpp b/GPU/Common/PresentationCommon.cpp index af8316b685..a5d72daff9 100644 --- a/GPU/Common/PresentationCommon.cpp +++ b/GPU/Common/PresentationCommon.cpp @@ -528,6 +528,8 @@ Draw::Pipeline *PresentationCommon::CreatePipeline(std::vector + +#include "Core/HW/GranularMixer.h" +#include "Core/HW/StereoResampler.h" + +extern StereoResampler g_resampler; +extern GranularMixer g_granular; diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 3b5d1d7045..a36eb376a1 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -673,6 +673,12 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { }); mixWithOthers->SetEnabledPtr(&g_Config.bEnableSound); #endif + audioSettings->Add(new ItemHeader(a->T("Audio"))); + + static const char *syncModes[] = { "Smooth", "Classic" }; + + audioSettings->Add(new PopupMultiChoice(&g_Config.iAudioSyncMode, a->T("Audio sync mode"), syncModes, 0, ARRAY_SIZE(syncModes), I18NCat::AUDIO, screenManager())); + audioSettings->Add(new CheckBox(&g_Config.bFillAudioGaps, a->T("Fill audio gaps"))); audioSettings->Add(new ItemHeader(a->T("Game volume"))); diff --git a/UI/ImDebugger/ImDebugger.cpp b/UI/ImDebugger/ImDebugger.cpp index 7720281559..d010a24f26 100644 --- a/UI/ImDebugger/ImDebugger.cpp +++ b/UI/ImDebugger/ImDebugger.cpp @@ -64,6 +64,7 @@ #include "UI/ImDebugger/ImDebugger.h" #include "UI/ImDebugger/ImGe.h" +#include "UI/AudioCommon.h" extern bool g_TakeScreenshot; static ImVec4 g_normalTextColor; @@ -1339,6 +1340,39 @@ void DrawMediaDecodersView(ImConfig &cfg, ImControl &control) { ImGui::End(); } + +void DrawAudioOut(ImConfig &cfg, ImControl &control) { + if (!ImGui::Begin("Audio output", &cfg.audioOutOpen)) { + ImGui::End(); + return; + } + + if (g_Config.iAudioSyncMode == (int)AudioSyncMode::GRANULAR) { + // Show granular stats + GranularStats stats; + g_granular.GetStats(&stats); + ImGui::Text("Granules"); + ImGui::Text("Read size: %0.2f", stats.smoothedReadSize); + ImGui::Text("Frame time estimate: %0.1f ms", stats.frameTimeEstimate * 1000.0f); + ImGui::Text("Queued samples target: %d", stats.queuedSamplesTarget); + ImGui::Text("Queued min/max: %d / %d", stats.queuedGranulesMin, stats.queuedGranulesMax); + ImGui::Text("Queued (smooth): %0.3f", stats.smoothedQueuedGranules); + ImGui::Text("Target: %d", stats.targetQueueSize); + ImGui::Text("Max: %d", stats.maxQueuedGranules); + ImGui::Text("Computed queue latency: %d ms", (int)(stats.smoothedQueuedGranules * (GranularMixer::GRANULE_SIZE * 1000.0 / 44100.0))); + ImGui::Text("Fade volume: %0.2f", stats.fadeVolume); + ImGui::Text("Looping: %s", BoolStr(stats.looping)); + ImGui::Text("Under/Over: %d / %d", stats.underruns, stats.overruns); + } else { + ImGui::Text("StereoResampler classic"); + char buf[1024]; + g_resampler.GetAudioDebugStats(buf, sizeof(buf)); + ImGui::Text("%s", buf); + } + + ImGui::End(); +} + void DrawAudioChannels(ImConfig &cfg, ImControl &control) { if (!ImGui::Begin("Raw audio channels", &cfg.audioChannelsOpen)) { ImGui::End(); @@ -2261,6 +2295,8 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUDebugInterface *gpuDebu ImGui::MenuItem("SasAudio mixer", nullptr, &cfg_.sasAudioOpen); ImGui::MenuItem("Raw audio channels", nullptr, &cfg_.audioChannelsOpen); ImGui::MenuItem("AV Decoder contexts", nullptr, &cfg_.mediaDecodersOpen); + ImGui::Separator(); + ImGui::MenuItem("Audio output / buffer", nullptr, &cfg_.audioOutOpen); ImGui::EndMenu(); } if (ImGui::BeginMenu("Network")) { @@ -2340,6 +2376,10 @@ void ImDebugger::Frame(MIPSDebugInterface *mipsDebug, GPUDebugInterface *gpuDebu DrawAudioChannels(cfg_, control); } + if (cfg_.audioOutOpen) { + DrawAudioOut(cfg_, control); + } + if (cfg_.sasAudioOpen) { DrawSasAudio(cfg_); } @@ -2589,6 +2629,7 @@ void ImConfig::SyncConfig(IniFile *ini, bool save) { sync.Sync("filesystemBrowserOpen", &filesystemBrowserOpen, false); sync.Sync("kernelObjectsOpen", &kernelObjectsOpen, false); sync.Sync("audioChannelsOpen", &audioChannelsOpen, false); + sync.Sync("audioOutOpen", &audioOutOpen, false); sync.Sync("texturesOpen", &texturesOpen, false); sync.Sync("debugStatsOpen", &debugStatsOpen, false); sync.Sync("geDebuggerOpen", &geDebuggerOpen, false); diff --git a/UI/ImDebugger/ImDebugger.h b/UI/ImDebugger/ImDebugger.h index e898325338..d069feca16 100644 --- a/UI/ImDebugger/ImDebugger.h +++ b/UI/ImDebugger/ImDebugger.h @@ -87,6 +87,7 @@ struct ImConfig { bool atracToolOpen; bool memViewOpen[4]; bool luaConsoleOpen; + bool audioOutOpen; // HLE explorer settings // bool filterByUsed = true; diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 2a4284a601..8254121cf3 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -161,6 +161,7 @@ + @@ -429,6 +430,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index ccb9d16891..27029b01ad 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -165,6 +165,7 @@ + @@ -679,8 +680,12 @@ +<<<<<<< HEAD +======= + +>>>>>>> cfb162d436 (Integrate Dolphin's granule based audio resampler.) - \ No newline at end of file + diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 8cb25082ba..55f67d7d15 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -585,6 +585,7 @@ EXEC_AND_LIB_FILES := \ $(SRC)/Core/HW/SasAudio.cpp.arm \ $(SRC)/Core/HW/SasReverb.cpp.arm \ $(SRC)/Core/HW/StereoResampler.cpp.arm \ + $(SRC)/Core/HW/GranularMixer.cpp.arm \ $(SRC)/Core/ControlMapper.cpp \ $(SRC)/Core/Core.cpp \ $(SRC)/Core/Compatibility.cpp \ diff --git a/build.gradle b/build.gradle index 229c4c755b..c1bc539906 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.12.0' + classpath 'com.android.tools.build:gradle:8.12.1' } } diff --git a/ios/iOSCoreAudio.mm b/ios/iOSCoreAudio.mm index 3216ec0635..e8f7619583 100644 --- a/ios/iOSCoreAudio.mm +++ b/ios/iOSCoreAudio.mm @@ -74,7 +74,7 @@ void iOSCoreAudioSetDisplayConnected(bool connected) { iOSCoreAudioUpdateSession(); } -void NativeMix(short *audio, int numSamples, int sampleRate, void *userdata); +void NativeMix(short *audio, int numSamples, int sampleRateHz, void *userdata); OSStatus iOSCoreAudioCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, diff --git a/libretro/libretro.cpp b/libretro/libretro.cpp index 9364dc4539..8f9246fa1c 100644 --- a/libretro/libretro.cpp +++ b/libretro/libretro.cpp @@ -1934,10 +1934,8 @@ void retro_cheat_set(unsigned index, bool enabled, const char *code) { } } -int64_t System_GetPropertyInt(SystemProperty prop) -{ - switch (prop) - { +int64_t System_GetPropertyInt(SystemProperty prop) { + switch (prop) { case SYSPROP_AUDIO_SAMPLE_RATE: return SAMPLERATE; #if PPSSPP_PLATFORM(ANDROID)